text: import copy
  import itertools as it
  import random
  import sys
  from functools import wraps
  
  import moderngl
  import numpy as np
  from colour import Color
  
  from .. import config
  from ..constants import *
  from ..utils.bezier import interpolate
  from ..utils.color import *
  
  # from ..utils.iterables import batch_by_property
  from ..utils.iterables import (
      batch_by_property,
      list_update,
      listify,
      make_even,
      resize_array,
      resize_preserving_order,
      resize_with_interpolation,
  )
  from ..utils.paths import straight_path
  from ..utils.simple_functions import get_parameters
  from ..utils.space_ops import (
      angle_between_vectors,
      angle_of_vector,
      normalize,
      rotation_matrix_transpose,
  )
  
  
  class OpenGLMobject:
      """
      Mathematical Object
      """
  
      shader_dtype = [
          ("point", np.float32, (3,)),
      ]
      shader_folder = ""
  
      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.TRIANGLE_STRIP,
          texture_paths=None,
          depth_test=False,
          # If true, the mobject will not get rotated according to camera position
          is_fixed_in_frame=False,
          # Must match in attributes of vert shader
          # Event listener
          listen_to_events=False,
          **kwargs,
      ):
  
          self.color = 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
          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 = is_fixed_in_frame
          # Must match in attributes of vert shader
          # Event listener
          self.listen_to_events = listen_to_events
  
          self.submobjects = []
          self.parents = []
          self.family = [self]
          self.locked_data_keys = set()
          self.needs_new_bounding_box = True
  
          self.init_data()
          self.init_uniforms()
          self.init_updaters()
          # self.init_event_listners()
          self.init_points()
          self.init_colors()
  
          self.shader_indices = None
  
          if self.depth_test:
              self.apply_depth_test()
  
      def __str__(self):
          return self.__class__.__name__
  
      def init_data(self):
          self.data = {
              "points": np.zeros((0, 3)),
              "bounding_box": np.zeros((3, 3)),
              "rgbas": np.zeros((1, 4)),
          }
  
      def init_uniforms(self):
          self.uniforms = {
              "is_fixed_in_frame": float(self.is_fixed_in_frame),
              "gloss": self.gloss,
              "shadow": self.shadow,
          }
  
      def init_colors(self):
          self.set_color(self.color, self.opacity)
  
      def init_points(self):
          # Typically implemented in subclass, unlpess purposefully left blank
          pass
  
      def set_data(self, data):
          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?
          return self
  
      @property
      def animate(self):
          # Borrowed from https://github.com/ManimCommunity/manim/
          return _AnimationBuilder(self)
  
      @property
      def width(self):
          """The width of the mobject.
  
          Returns
          -------
          :class:`float`
  
          Examples
          --------
          .. manim:: WidthExample
  
              class WidthExample(Scene):
                  def construct(self):
                      decimal = DecimalNumber().to_edge(UP)
                      rect = Rectangle(color=BLUE)
                      rect_copy = rect.copy().set_stroke(GRAY, opacity=0.5)
  
                      decimal.add_updater(lambda d: d.set_value(rect.width))
  
                      self.add(rect_copy, rect, decimal)
                      self.play(rect.animate.set(width=7))
                      self.wait()
  
          See also
          --------
          :meth:`length_over_dim`
  
          """
  
          # Get the length across the X dimension
          return self.length_over_dim(0)
  
      # Only these methods should directly affect points
      @width.setter
      def width(self, value):
          self.rescale_to_fit(value, 0, stretch=False)
  
      @property
      def height(self):
          """The height of the mobject.
  
          Returns
          -------
          :class:`float`
  
          Examples
          --------
          .. manim:: HeightExample
  
              class HeightExample(Scene):
                  def construct(self):
                      decimal = DecimalNumber().to_edge(UP)
                      rect = Rectangle(color=BLUE)
                      rect_copy = rect.copy().set_stroke(GRAY, opacity=0.5)
  
                      decimal.add_updater(lambda d: d.set_value(rect.height))
  
                      self.add(rect_copy, rect, decimal)
                      self.play(rect.animate.set(height=5))
                      self.wait()
  
          See also
          --------
          :meth:`length_over_dim`
  
          """
  
          # Get the length across the Y dimension
          return self.length_over_dim(1)
  
      @height.setter
      def height(self, value):
          self.rescale_to_fit(value, 1, stretch=False)
  
      @property
      def depth(self):
          """The depth of the mobject.
  
          Returns
          -------
          :class:`float`
  
          See also
          --------
          :meth:`length_over_dim`
  
          """
  
          # Get the length across the Z dimension
          return self.length_over_dim(2)
  
      @depth.setter
      def depth(self, value):
          self.rescale_to_fit(value, 2, stretch=False)
  
      def resize_points(self, new_length, resize_func=resize_array):
          if new_length != len(self.data["points"]):
              self.data["points"] = resize_func(self.data["points"], new_length)
          self.refresh_bounding_box()
          return self
  
      def set_points(self, points):
          if len(points) == len(self.data["points"]):
              self.data["points"][:] = points
          elif isinstance(points, np.ndarray):
              self.data["points"] = points.copy()
          else:
              self.data["points"] = np.array(points)
          self.refresh_bounding_box()
          return self
  
      def append_points(self, new_points):
          self.data["points"] = np.vstack([self.data["points"], new_points])
          self.refresh_bounding_box()
          return self
  
      def reverse_points(self):
          for mob in self.get_family():
              for key in mob.data:
                  mob.data[key] = mob.data[key][::-1]
          return self
  
      def apply_points_function(
          self, func, about_point=None, about_edge=ORIGIN, works_on_bounding_box=False
      ):
          if about_point is None and about_edge is not None:
              about_point = self.get_bounding_box_point(about_edge)
  
          for mob in self.get_family():
              arrs = []
              if mob.has_points():
                  arrs.append(mob.get_points())
              if works_on_bounding_box:
                  arrs.append(mob.get_bounding_box())
  
              for arr in arrs:
                  if about_point is None:
                      arr[:] = func(arr)
                  else:
                      arr[:] = func(arr - about_point) + about_point
  
          if not works_on_bounding_box:
              self.refresh_bounding_box(recurse_down=True)
          else:
              for parent in self.parents:
                  parent.refresh_bounding_box()
          return self
  
      # Others related to points
  
      def match_points(self, mobject):
          self.set_points(mobject.get_points())
  
      def get_points(self):
          return self.data["points"]
  
      def clear_points(self):
          self.resize_points(0)
  
      def get_num_points(self):
          return len(self.data["points"])
  
      def get_all_points(self):
          if self.submobjects:
              return np.vstack([sm.get_points() for sm in self.get_family()])
          else:
              return self.get_points()
  
      def has_points(self):
          return self.get_num_points() > 0
  
      def get_bounding_box(self):
          if self.needs_new_bounding_box:
              self.data["bounding_box"] = self.compute_bounding_box()
              self.needs_new_bounding_box = False
          return self.data["bounding_box"]
  
      def compute_bounding_box(self):
          all_points = np.vstack(
              [
                  self.get_points(),
                  *(
                      mob.get_bounding_box()
                      for mob in self.get_family()[1:]
                      if mob.has_points()
                  ),
              ]
          )
          if len(all_points) == 0:
              return np.zeros((3, self.dim))
          else:
              # Lower left and upper right corners
              mins = all_points.min(0)
              maxs = all_points.max(0)
              mids = (mins + maxs) / 2
              return np.array([mins, mids, maxs])
  
      def refresh_bounding_box(self, recurse_down=False, recurse_up=True):
          for mob in self.get_family(recurse_down):
              mob.needs_new_bounding_box = True
          if recurse_up:
              for parent in self.parents:
                  parent.refresh_bounding_box()
          return self
  
      def is_point_touching(self, point, buff=MED_SMALL_BUFF):
          bb = self.get_bounding_box()
          mins = bb[0] - buff
          maxs = bb[2] + buff
          return (point >= mins).all() and (point <= maxs).all()
  
      # Family matters
  
      def __getitem__(self, value):
          if isinstance(value, slice):
              GroupClass = self.get_group_class()
              return GroupClass(*self.split().__getitem__(value))
          return self.split().__getitem__(value)
  
      def __iter__(self):
          return iter(self.split())
  
      def __len__(self):
          return len(self.split())
  
      def split(self):
          return self.submobjects
  
      def assemble_family(self):
          sub_families = (sm.get_family() for sm in self.submobjects)
          self.family = [self, *it.chain(*sub_families)]
          self.refresh_has_updater_status()
          self.refresh_bounding_box()
          for parent in self.parents:
              parent.assemble_family()
          return self
  
      def get_family(self, recurse=True):
          if recurse:
              return self.family
          else:
              return [self]
  
      def family_members_with_points(self):
          return [m for m in self.get_family() if m.has_points()]
  
      def add(self, *mobjects):
          if self in mobjects:
              raise Exception("Mobject cannot contain self")
          for mobject in mobjects:
              if mobject not in self.submobjects:
                  self.submobjects.append(mobject)
              if self not in mobject.parents:
                  mobject.parents.append(self)
          self.assemble_family()
          return self
  
      def remove(self, *mobjects):
          for mobject in mobjects:
              if mobject in self.submobjects:
                  self.submobjects.remove(mobject)
              if self in mobject.parents:
                  mobject.parents.remove(self)
          self.assemble_family()
          return self
  
      def add_to_back(self, *mobjects):
          self.set_submobjects(list_update(mobjects, self.submobjects))
          return self
  
      def replace_submobject(self, index, new_submob):
          old_submob = self.submobjects[index]
          if self in old_submob.parents:
              old_submob.parents.remove(self)
          self.submobjects[index] = new_submob
          self.assemble_family()
          return self
  
      def set_submobjects(self, submobject_list):
          self.remove(*self.submobjects)
          self.add(*submobject_list)
          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
          --------
  
          .. manim:: InvertSumobjectsExample
  
              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))
          """
          if recursive:
              for submob in self.submobjects:
                  submob.invert(recursive=True)
          list.reverse(self.submobjects)
          self.assemble_family()
  
      def digest_mobject_attrs(self):
          """
          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):
          for m1, m2 in zip(self.submobjects, self.submobjects[1:]):
              m2.next_to(m1, direction, **kwargs)
          if center:
              self.center()
          return self
  
      def arrange_in_grid(
          self,
          n_rows=None,
          n_cols=None,
          buff=None,
          h_buff=None,
          v_buff=None,
          buff_ratio=None,
          h_buff_ratio=0.5,
          v_buff_ratio=0.5,
          aligned_edge=ORIGIN,
          fill_rows_first=True,
      ):
          submobs = self.submobjects
          if n_rows is None and n_cols is None:
              n_rows = int(np.sqrt(len(submobs)))
          if n_rows is None:
              n_rows = len(submobs) // n_cols
          if n_cols is None:
              n_cols = len(submobs) // n_rows
  
          if buff is not None:
              h_buff = buff
              v_buff = buff
          else:
              if buff_ratio is not None:
                  v_buff_ratio = buff_ratio
                  h_buff_ratio = buff_ratio
              if h_buff is None:
                  h_buff = h_buff_ratio * self[0].get_width()
              if v_buff is None:
                  v_buff = v_buff_ratio * self[0].get_height()
  
          x_unit = h_buff + max([sm.get_width() for sm in submobs])
          y_unit = v_buff + max([sm.get_height() for sm in submobs])
  
          for index, sm in enumerate(submobs):
              if fill_rows_first:
                  x, y = index % n_cols, index // n_cols
              else:
                  x, y = index // n_rows, index % n_rows
              sm.move_to(ORIGIN, aligned_edge)
              sm.shift(x * x_unit * RIGHT + y * y_unit * DOWN)
          self.center()
          return self
  
      def get_grid(self, n_rows, n_cols, height=None, **kwargs):
          """
          Returns a new mobject containing multiple copies of this one
          arranged in a grid
          """
          grid = self.get_group_class()(*(self.copy() for n in range(n_rows * n_cols)))
          grid.arrange_in_grid(n_rows, n_cols, **kwargs)
          if height is not None:
              grid.set_height(height)
          return grid
  
      def sort(self, point_to_num_func=lambda p: p[0], submob_func=None):
          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()))
          return self
  
      def shuffle(self, recurse=False):
          if recurse:
              for submob in self.submobjects:
                  submob.shuffle(recurse=True)
          random.shuffle(self.submobjects)
          self.assemble_family()
          return self
  
      # Copying
  
      def copy(self, shallow: bool = False):
          """Copies the mobject.
  
          Parameters
          ----------
          shallow
              Controls whether a shallow copy is returned.
          """
          if not shallow:
              return self.deepcopy()
  
          # TODO, either justify reason for shallow copy, or
          # remove this redundancy everywhere
          # return self.deepcopy()
  
          parents = self.parents
          self.parents = []
          copy_mobject = copy.copy(self)
          self.parents = parents
  
          copy_mobject.data = dict(self.data)
          for key in self.data:
              copy_mobject.data[key] = self.data[key].copy()
  
          # TODO, are uniforms ever numpy arrays?
          copy_mobject.uniforms = dict(self.uniforms)
  
          copy_mobject.submobjects = []
          copy_mobject.add(*[sm.copy() for sm in self.submobjects])
          copy_mobject.match_updaters(self)
  
          copy_mobject.needs_new_bounding_box = self.needs_new_bounding_box
  
          # Make sure any mobject or numpy array attributes are copied
          family = self.get_family()
          for attr, value in list(self.__dict__.items()):
              if (
                  isinstance(value, OpenGLMobject)
                  and value in family
                  and value is not self
              ):
                  setattr(copy_mobject, attr, value.copy())
              if isinstance(value, np.ndarray):
                  setattr(copy_mobject, attr, value.copy())
              # if isinstance(value, ShaderWrapper):
              #     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=False):
          self.target = None  # Prevent exponential explosion
          if use_deepcopy:
              self.target = self.deepcopy()
          else:
              self.target = self.copy()
          return self.target
  
      def save_state(self, use_deepcopy=False):
          if hasattr(self, "saved_state"):
              # Prevent exponential growth of data
              self.saved_state = None
          if use_deepcopy:
              self.saved_state = self.deepcopy()
          else:
              self.saved_state = self.copy()
          return self
  
      def restore(self):
          if not hasattr(self, "saved_state") or self.save_state is None:
              raise Exception("Trying to restore without having saved")
          self.become(self.saved_state)
          return self
  
      # Updating
  
      def init_updaters(self):
          self.time_based_updaters = []
          self.non_time_updaters = []
          self.has_updaters = False
          self.updating_suspended = False
  
      def update(self, dt=0, recurse=True):
          if not self.has_updaters or self.updating_suspended:
              return self
          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):
          return self.time_based_updaters
  
      def has_time_based_updater(self):
          return len(self.time_based_updaters) > 0
  
      def get_updaters(self):
          return self.time_based_updaters + self.non_time_updaters
  
      def get_family_updaters(self):
          return list(it.chain(*[sm.get_updaters() for sm in self.get_family()]))
  
      def add_updater(self, update_function, index=None, call_updater=True):
          if "dt" in get_parameters(update_function):
              updater_list = self.time_based_updaters
          else:
              updater_list = self.non_time_updaters
  
          if index is None:
              updater_list.append(update_function)
          else:
              updater_list.insert(index, update_function)
  
          self.refresh_has_updater_status()
          if call_updater:
              self.update()
          return self
  
      def remove_updater(self, update_function):
          for updater_list in [self.time_based_updaters, self.non_time_updaters]:
              while update_function in updater_list:
                  updater_list.remove(update_function)
          self.refresh_has_updater_status()
          return self
  
      def clear_updaters(self, recurse=True):
          self.time_based_updaters = []
          self.non_time_updaters = []
          self.refresh_has_updater_status()
          if recurse:
              for submob in self.submobjects:
                  submob.clear_updaters()
          return self
  
      def match_updaters(self, mobject):
          self.clear_updaters()
          for updater in mobject.get_updaters():
              self.add_updater(updater)
          return self
  
      def suspend_updating(self, recurse=True):
          self.updating_suspended = True
          if recurse:
              for submob in self.submobjects:
                  submob.suspend_updating(recurse)
          return self
  
      def resume_updating(self, recurse=True, call_updater=True):
          self.updating_suspended = False
          if recurse:
              for submob in self.submobjects:
                  submob.resume_updating(recurse)
          for parent in self.parents:
              parent.resume_updating(recurse=False, call_updater=False)
          if call_updater:
              self.update(dt=0, recurse=recurse)
          return self
  
      def refresh_has_updater_status(self):
          self.has_updaters = any(mob.get_updaters() for mob in self.get_family())
          return self
  
      # Transforming operations
  
      def shift(self, vector):
          self.apply_points_function(
              lambda points: points + vector,
              about_edge=None,
              works_on_bounding_box=True,
          )
          return self
  
      def scale(self, scale_factor, **kwargs):
          """
          Default behavior is to scale about the center of the mobject.
          The argument about_edge can be a vector, indicating which side of
          the mobject to scale about, e.g., mob.scale(about_edge = RIGHT)
          scales about mob.get_right().
  
          Otherwise, if about_point is given a value, scaling is done with
          respect to that point.
          """
          self.apply_points_function(
              lambda points: scale_factor * points, works_on_bounding_box=True, **kwargs
          )
          return self
  
      def stretch(self, factor, dim, **kwargs):
          def func(points):
              points[:, dim] *= factor
              return points
  
          self.apply_points_function(func, works_on_bounding_box=True, **kwargs)
          return self
  
      def rotate_about_origin(self, angle, axis=OUT):
          return self.rotate(angle, axis, about_point=ORIGIN)
  
      def rotate(
          self,
          angle,
          axis=OUT,
          **kwargs,
      ):
          rot_matrix_T = rotation_matrix_transpose(angle, axis)
          self.apply_points_function(
              lambda points: np.dot(points, rot_matrix_T), **kwargs
          )
          return self
  
      def flip(self, axis=UP, **kwargs):
          return self.rotate(TAU / 2, axis, **kwargs)
  
      def apply_function(self, function, **kwargs):
          # Default to applying matrix about the origin, not mobjects center
          if len(kwargs) == 0:
              kwargs["about_point"] = ORIGIN
          self.apply_points_function(
              lambda points: np.array([function(p) for p in points]), **kwargs
          )
          return self
  
      def apply_function_to_position(self, function):
          self.move_to(function(self.get_center()))
          return self
  
      def apply_function_to_submobject_positions(self, function):
          for submob in self.submobjects:
              submob.apply_function_to_position(function)
          return self
  
      def apply_matrix(self, matrix, **kwargs):
          # Default to applying matrix about the origin, not mobjects center
          if ("about_point" not in kwargs) and ("about_edge" not in kwargs):
              kwargs["about_point"] = ORIGIN
          full_matrix = np.identity(self.dim)
          matrix = np.array(matrix)
          full_matrix[: matrix.shape[0], : matrix.shape[1]] = matrix
          self.apply_points_function(
              lambda points: np.dot(points, full_matrix.T), **kwargs
          )
          return self
  
      def apply_complex_function(self, function, **kwargs):
          def R3_func(point):
              x, y, z = point
              xy_complex = function(complex(x, y))
              return [xy_complex.real, xy_complex.imag, z]
  
          return self.apply_function(R3_func)
  
      def wag(self, direction=RIGHT, axis=DOWN, wag_factor=1.0):
          for mob in self.family_members_with_points():
              alphas = np.dot(mob.get_points(), np.transpose(axis))
              alphas -= min(alphas)
              alphas /= max(alphas)
              alphas = alphas ** wag_factor
              mob.set_points(
                  mob.get_points()
                  + np.dot(
                      alphas.reshape((len(alphas), 1)),
                      np.array(direction).reshape((1, mob.dim)),
                  )
              )
          return self
  
      # Positioning methods
  
      def center(self):
          self.shift(-self.get_center())
          return self
  
      def align_on_border(self, direction, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER):
          """
          Direction just needs to be a vector pointing towards side or
          corner in the 2d plane.
          """
          target_point = np.sign(direction) * (
              config["frame_x_radius"],
              config["frame_y_radius"],
              0,
          )
          point_to_align = self.get_bounding_box_point(direction)
          shift_val = target_point - point_to_align - buff * np.array(direction)
          shift_val = shift_val * abs(np.sign(direction))
          self.shift(shift_val)
          return self
  
      def to_corner(self, corner=LEFT + DOWN, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER):
          return self.align_on_border(corner, buff)
  
      def to_edge(self, edge=LEFT, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER):
          return self.align_on_border(edge, buff)
  
      def next_to(
          self,
          mobject_or_point,
          direction=RIGHT,
          buff=DEFAULT_MOBJECT_TO_MOBJECT_BUFFER,
          aligned_edge=ORIGIN,
          submobject_to_align=None,
          index_of_submobject_to_align=None,
          coor_mask=np.array([1, 1, 1]),
      ):
          if isinstance(mobject_or_point, OpenGLMobject):
              mob = mobject_or_point
              if index_of_submobject_to_align is not None:
                  target_aligner = mob[index_of_submobject_to_align]
              else:
                  target_aligner = mob
              target_point = target_aligner.get_bounding_box_point(
                  aligned_edge + direction
              )
          else:
              target_point = mobject_or_point
          if submobject_to_align is not None:
              aligner = submobject_to_align
          elif index_of_submobject_to_align is not None:
              aligner = self[index_of_submobject_to_align]
          else:
              aligner = self
          point_to_align = aligner.get_bounding_box_point(aligned_edge - direction)
          self.shift((target_point - point_to_align + buff * direction) * coor_mask)
          return self
  
      def shift_onto_screen(self, **kwargs):
          space_lengths = [config["frame_x_radius"], config["frame_y_radius"]]
          for vect in UP, DOWN, LEFT, RIGHT:
              dim = np.argmax(np.abs(vect))
              buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_EDGE_BUFFER)
              max_val = space_lengths[dim] - buff
              edge_center = self.get_edge_center(vect)
              if np.dot(edge_center, vect) > max_val:
                  self.to_edge(vect, **kwargs)
          return self
  
      def is_off_screen(self):
          if self.get_left()[0] > config["frame_x_radius"]:
              return True
          if self.get_right()[0] < -config["frame_x_radius"]:
              return True
          if self.get_bottom()[1] > config["frame_y_radius"]:
              return True
          if self.get_top()[1] < -config["frame_y_radius"]:
              return True
          return False
  
      def stretch_about_point(self, factor, dim, point):
          return self.stretch(factor, dim, about_point=point)
  
      def stretch_in_place(self, factor, dim):
          # Now redundant with stretch
          return self.stretch(factor, dim)
  
      def rescale_to_fit(self, length, dim, stretch=False, **kwargs):
          old_length = self.length_over_dim(dim)
          if old_length == 0:
              return self
          if stretch:
              self.stretch(length / old_length, dim, **kwargs)
          else:
              self.scale(length / old_length, **kwargs)
          return self
  
      def stretch_to_fit_width(self, width, **kwargs):
          return self.rescale_to_fit(width, 0, stretch=True, **kwargs)
  
      def stretch_to_fit_height(self, height, **kwargs):
          return self.rescale_to_fit(height, 1, stretch=True, **kwargs)
  
      def stretch_to_fit_depth(self, depth, **kwargs):
          return self.rescale_to_fit(depth, 1, stretch=True, **kwargs)
  
      def set_width(self, width, stretch=False, **kwargs):
          return self.rescale_to_fit(width, 0, stretch=stretch, **kwargs)
  
      scale_to_fit_width = set_width
  
      def set_height(self, height, stretch=False, **kwargs):
          return self.rescale_to_fit(height, 1, stretch=stretch, **kwargs)
  
      scale_to_fit_height = set_height
  
      def set_depth(self, depth, stretch=False, **kwargs):
          return self.rescale_to_fit(depth, 2, stretch=stretch, **kwargs)
  
      def set_coord(self, value, dim, direction=ORIGIN):
          curr = self.get_coord(dim, direction)
          shift_vect = np.zeros(self.dim)
          shift_vect[dim] = value - curr
          self.shift(shift_vect)
          return self
  
      def set_x(self, x, direction=ORIGIN):
          return self.set_coord(x, 0, direction)
  
      def set_y(self, y, direction=ORIGIN):
          return self.set_coord(y, 1, direction)
  
      def set_z(self, z, direction=ORIGIN):
          return self.set_coord(z, 2, direction)
  
      def space_out_submobjects(self, factor=1.5, **kwargs):
          self.scale(factor, **kwargs)
          for submob in self.submobjects:
              submob.scale(1.0 / factor)
          return self
  
      def move_to(
          self, point_or_mobject, aligned_edge=ORIGIN, coor_mask=np.array([1, 1, 1])
      ):
          if isinstance(point_or_mobject, OpenGLMobject):
              target = point_or_mobject.get_bounding_box_point(aligned_edge)
          else:
              target = point_or_mobject
          point_to_align = self.get_bounding_box_point(aligned_edge)
          self.shift((target - point_to_align) * coor_mask)
          return self
  
      def replace(self, mobject, dim_to_match=0, stretch=False):
          if not mobject.get_num_points() and not mobject.submobjects:
              self.scale(0)
              return self
          if stretch:
              for i in range(self.dim):
                  self.rescale_to_fit(mobject.length_over_dim(i), i, stretch=True)
          else:
              self.rescale_to_fit(
                  mobject.length_over_dim(dim_to_match), dim_to_match, stretch=False
              )
          self.shift(mobject.get_center() - self.get_center())
          return self
  
      def surround(self, mobject, dim_to_match=0, stretch=False, buff=MED_SMALL_BUFF):
          self.replace(mobject, dim_to_match, stretch)
          length = mobject.length_over_dim(dim_to_match)
          self.scale((length + buff) / length)
          return self
  
      def put_start_and_end_on(self, start, end):
          curr_start, curr_end = self.get_start_and_end()
          curr_vect = curr_end - curr_start
          if np.all(curr_vect == 0):
              raise Exception("Cannot position endpoints of closed loop")
          target_vect = np.array(end) - np.array(start)
          axis = (
              normalize(np.cross(curr_vect, target_vect))
              if np.linalg.norm(np.cross(curr_vect, target_vect)) != 0
              else OUT
          )
          self.scale(
              np.linalg.norm(target_vect) / np.linalg.norm(curr_vect),
              about_point=curr_start,
          )
          self.rotate(
              angle_between_vectors(curr_vect, target_vect),
              about_point=curr_start,
              axis=axis,
          )
          self.shift(start - curr_start)
          return self
  
      # Color functions
  
      def set_rgba_array(self, color=None, opacity=None, name="rgbas", recurse=True):
          if color is not None:
              rgbs = np.array([color_to_rgb(c) for c in listify(color)])
          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], 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], len(opacities))
                  mob.data[name][:, 3] = 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()
          return self
  
      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
          # in case they implement set_color differently
          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)
          if recurse:
              for submob in self.submobjects:
                  submob.set_opacity(opacity, recurse=True)
          return self
  
      def get_color(self):
          return rgb_to_hex(self.data["rgbas"][0, :3])
  
      def get_opacity(self):
          return self.data["rgbas"][0, 3]
  
      def set_color_by_gradient(self, *colors):
          self.set_submobject_colors_by_gradient(*colors)
          return self
  
      def set_submobject_colors_by_gradient(self, *colors):
          if len(colors) == 0:
              raise Exception("Need at least one color")
          elif len(colors) == 1:
              return self.set_color(*colors)
  
          # mobs = self.family_members_with_points()
          mobs = self.submobjects
          new_colors = color_gradient(colors, len(mobs))
  
          for mob, color in zip(mobs, new_colors):
              mob.set_color(color)
          return self
  
      def fade(self, darkness=0.5, recurse=True):
          self.set_opacity(1.0 - darkness, recurse=recurse)
  
      def get_gloss(self):
          return self.uniforms["gloss"]
  
      def set_gloss(self, gloss, recurse=True):
          for mob in self.get_family(recurse):
              mob.uniforms["gloss"] = gloss
          return self
  
      def get_shadow(self):
          return self.uniforms["shadow"]
  
      def set_shadow(self, shadow, recurse=True):
          for mob in self.get_family(recurse):
              mob.uniforms["shadow"] = shadow
          return self
  
      # Background rectangle
  
      def add_background_rectangle(self, color=None, opacity=0.75, **kwargs):
          # TODO, this does not behave well when the mobject has points,
          # since it gets displayed on top
          from ..mobject.shape_matchers import BackgroundRectangle
  
          self.background_rectangle = BackgroundRectangle(
              self, color=color, fill_opacity=opacity, **kwargs
          )
          self.add_to_back(self.background_rectangle)
          return self
  
      def add_background_rectangle_to_submobjects(self, **kwargs):
          for submobject in self.submobjects:
              submobject.add_background_rectangle(**kwargs)
          return self
  
      def add_background_rectangle_to_family_members_with_points(self, **kwargs):
          for mob in self.family_members_with_points():
              mob.add_background_rectangle(**kwargs)
          return self
  
      # Getters
  
      def get_bounding_box_point(self, direction):
          bb = self.get_bounding_box()
          indices = (np.sign(direction) + 1).astype(int)
          return np.array([bb[indices[i]][i] for i in range(3)])
  
      def get_edge_center(self, direction):
          return self.get_bounding_box_point(direction)
  
      def get_corner(self, direction):
          return self.get_bounding_box_point(direction)
  
      def get_center(self):
          return self.get_bounding_box()[1]
  
      def get_center_of_mass(self):
          return self.get_all_points().mean(0)
  
      def get_boundary_point(self, direction):
          all_points = self.get_all_points()
          boundary_directions = all_points - self.get_center()
          norms = np.linalg.norm(boundary_directions, axis=1)
          boundary_directions /= np.repeat(norms, 3).reshape((len(norms), 3))
          index = np.argmax(np.dot(boundary_directions, np.array(direction).T))
          return all_points[index]
  
      def get_continuous_bounding_box_point(self, direction):
          dl, center, ur = self.get_bounding_box()
          corner_vect = ur - center
          return center + direction / np.max(
              np.abs(
                  np.true_divide(
                      direction,
                      corner_vect,
                      out=np.zeros(len(direction)),
                      where=((corner_vect) != 0),
                  )
              )
          )
  
      def get_top(self):
          return self.get_edge_center(UP)
  
      def get_bottom(self):
          return self.get_edge_center(DOWN)
  
      def get_right(self):
          return self.get_edge_center(RIGHT)
  
      def get_left(self):
          return self.get_edge_center(LEFT)
  
      def get_zenith(self):
          return self.get_edge_center(OUT)
  
      def get_nadir(self):
          return self.get_edge_center(IN)
  
      def length_over_dim(self, dim):
          bb = self.get_bounding_box()
          return abs((bb[2] - bb[0])[dim])
  
      def get_width(self):
          return self.length_over_dim(0)
  
      def get_height(self):
          return self.length_over_dim(1)
  
      def get_depth(self):
          return self.length_over_dim(2)
  
      def get_coord(self, dim, direction=ORIGIN):
          """
          Meant to generalize get_x, get_y, get_z
          """
          return self.get_bounding_box_point(direction)[dim]
  
      def get_x(self, direction=ORIGIN):
          return self.get_coord(0, direction)
  
      def get_y(self, direction=ORIGIN):
          return self.get_coord(1, direction)
  
      def get_z(self, direction=ORIGIN):
          return self.get_coord(2, direction)
  
      def get_start(self):
          self.throw_error_if_no_points()
          return np.array(self.get_points()[0])
  
      def get_end(self):
          self.throw_error_if_no_points()
          return np.array(self.get_points()[-1])
  
      def get_start_and_end(self):
          return self.get_start(), self.get_end()
  
      def point_from_proportion(self, alpha):
          points = self.get_points()
          i, subalpha = integer_interpolate(0, len(points) - 1, alpha)
          return interpolate(points[i], points[i + 1], subalpha)
  
      def pfp(self, alpha):
          """Abbreviation for point_from_proportion"""
          return self.point_from_proportion(alpha)
  
      def get_pieces(self, n_pieces):
          template = self.copy()
          template.set_submobjects([])
          alphas = np.linspace(0, 1, n_pieces + 1)
          return OpenGLGroup(
              *[
                  template.copy().pointwise_become_partial(self, a1, a2)
                  for a1, a2 in zip(alphas[:-1], alphas[1:])
              ]
          )
  
      def get_z_index_reference_point(self):
          # TODO, better place to define default z_index_group?
          z_index_group = getattr(self, "z_index_group", self)
          return z_index_group.get_center()
  
      # Match other mobject properties
  
      def match_color(self, mobject):
          return self.set_color(mobject.get_color())
  
      def match_dim_size(self, mobject, dim, **kwargs):
          return self.rescale_to_fit(mobject.length_over_dim(dim), dim, **kwargs)
  
      def match_width(self, mobject, **kwargs):
          return self.match_dim_size(mobject, 0, **kwargs)
  
      def match_height(self, mobject, **kwargs):
          return self.match_dim_size(mobject, 1, **kwargs)
  
      def match_depth(self, mobject, **kwargs):
          return self.match_dim_size(mobject, 2, **kwargs)
  
      def match_coord(self, mobject, dim, direction=ORIGIN):
          return self.set_coord(
              mobject.get_coord(dim, direction),
              dim=dim,
              direction=direction,
          )
  
      def match_x(self, mobject, direction=ORIGIN):
          return self.match_coord(mobject, 0, direction)
  
      def match_y(self, mobject, direction=ORIGIN):
          return self.match_coord(mobject, 1, direction)
  
      def match_z(self, mobject, direction=ORIGIN):
          return self.match_coord(mobject, 2, direction)
  
      def align_to(self, mobject_or_point, direction=ORIGIN):
          """
          Examples:
          mob1.align_to(mob2, UP) moves mob1 vertically so that its
          top edge lines ups with mob2's top edge.
  
          mob1.align_to(mob2, alignment_vect = RIGHT) moves mob1
          horizontally so that it's center is directly above/below
          the center of mob2
          """
          if isinstance(mobject_or_point, OpenGLMobject):
              point = mobject_or_point.get_bounding_box_point(direction)
          else:
              point = mobject_or_point
  
          for dim in range(self.dim):
              if direction[dim] != 0:
                  self.set_coord(point[dim], dim, direction)
          return self
  
      def get_group_class(self):
          return OpenGLGroup
  
      # Alignment
  
      def align_data_and_family(self, mobject):
          self.align_family(mobject)
          self.align_data(mobject)
  
      def align_data(self, mobject):
          # In case any data arrays get resized when aligned to shader data
          # self.refresh_shader_data()
          for mob1, mob2 in zip(self.get_family(), mobject.get_family()):
              # Separate out how points are treated so that subclasses
              # can handle that case differently if they choose
              mob1.align_points(mob2)
              for key in mob1.data.keys() & mob2.data.keys():
                  if key == "points":
                      continue
                  arr1 = mob1.data[key]
                  arr2 = mob2.data[key]
                  if len(arr2) > len(arr1):
                      mob1.data[key] = resize_preserving_order(arr1, len(arr2))
                  elif len(arr1) > len(arr2):
                      mob2.data[key] = resize_preserving_order(arr2, len(arr1))
  
      def align_points(self, mobject):
          max_len = max(self.get_num_points(), mobject.get_num_points())
          for mob in (self, mobject):
              mob.resize_points(max_len, resize_func=resize_preserving_order)
          return self
  
      def align_family(self, mobject):
          mob1 = self
          mob2 = mobject
          n1 = len(mob1)
          n2 = len(mob2)
          if n1 != n2:
              mob1.add_n_more_submobjects(max(0, n2 - n1))
              mob2.add_n_more_submobjects(max(0, n1 - n2))
          # Recurse
          for sm1, sm2 in zip(mob1.submobjects, mob2.submobjects):
              sm1.align_family(sm2)
          return self
  
      def push_self_into_submobjects(self):
          copy = self.deepcopy()
          copy.set_submobjects([])
          self.resize_points(0)
          self.add(copy)
          return self
  
      def add_n_more_submobjects(self, n):
          if n == 0:
              return self
  
          curr = len(self.submobjects)
          if curr == 0:
              # If empty, simply add n point mobjects
              null_mob = self.copy()
              null_mob.set_points([self.get_center()])
              self.set_submobjects([null_mob.copy() for k in range(n)])
              return self
          target = curr + n
          repeat_indices = (np.arange(target) * curr) // target
          split_factors = [(repeat_indices == i).sum() for i in range(curr)]
          new_submobs = []
          for submob, sf in zip(self.submobjects, split_factors):
              new_submobs.append(submob)
              for _ in range(1, sf):
                  new_submob = submob.copy()
                  # If the submobject is at all transparent, then
                  # make the copy completely transparent
                  if submob.get_opacity() < 1:
                      new_submob.set_opacity(0)
                  new_submobs.append(new_submob)
          self.set_submobjects(new_submobs)
          return self
  
      # Interpolate
  
      def interpolate(self, mobject1, mobject2, alpha, path_func=straight_path):
          for key in self.data:
              if key in self.locked_data_keys:
                  continue
              if len(self.data[key]) == 0:
                  continue
              if key not in mobject1.data or key not in mobject2.data:
                  continue
  
              if key in ("points", "bounding_box"):
                  func = path_func
              else:
                  func = interpolate
  
              self.data[key][:] = func(mobject1.data[key], mobject2.data[key], alpha)
          for key in self.uniforms:
              self.uniforms[key] = interpolate(
                  mobject1.uniforms[key], mobject2.uniforms[key], alpha
              )
          return self
  
      def pointwise_become_partial(self, mobject, a, b):
          """
          Set points in such a way as to become only
          part of mobject.
          Inputs 0 <= a < b <= 1 determine what portion
          of mobject to become.
          """
          pass  # To implement in subclass
  
      def become(self, mobject):
          """
          Edit all data and submobjects to be identical
          to another mobject
          """
          self.align_family(mobject)
          for sm1, sm2 in zip(self.get_family(), mobject.get_family()):
              sm1.set_data(sm2.data)
              sm1.set_uniforms(sm2.uniforms)
          self.refresh_bounding_box(recurse_down=True)
          return self
  
      # Locking data
  
      def lock_data(self, keys):
          """
          To speed up some animations, particularly transformations,
          it can be handy to acknowledge which pieces of data
          won't change during the animation so that calls to
          interpolate can skip this, and so that it's not
          read into the shader_wrapper objects needlessly
          """
          if self.has_updaters:
              return
          # Be sure shader data has most up to date information
          self.refresh_shader_data()
          self.locked_data_keys = set(keys)
  
      def lock_matching_data(self, mobject1, mobject2):
          for sm, sm1, sm2 in zip(
              self.get_family(), mobject1.get_family(), mobject2.get_family()
          ):
              keys = sm.data.keys() & sm1.data.keys() & sm2.data.keys()
              sm.lock_data(
                  list(
                      filter(
                          lambda key: np.all(sm1.data[key] == sm2.data[key]),
                          keys,
                      )
                  )
              )
          return self
  
      def unlock_data(self):
          for mob in self.get_family():
              mob.locked_data_keys = set()
  
      # Operations touching shader uniforms
  
      def affects_shader_info_id(func):
          @wraps(func)
          def wrapper(self):
              for mob in self.get_family():
                  func(mob)
                  # mob.refresh_shader_wrapper_id()
              return self
  
          return wrapper
  
      @affects_shader_info_id
      def fix_in_frame(self):
          self.uniforms["is_fixed_in_frame"] = 1.0
          return self
  
      @affects_shader_info_id
      def unfix_from_frame(self):
          self.uniforms["is_fixed_in_frame"] = 0.0
          return self
  
      @affects_shader_info_id
      def apply_depth_test(self):
          self.depth_test = True
          return self
  
      @affects_shader_info_id
      def deactivate_depth_test(self):
          self.depth_test = False
          return self
  
      # Shader code manipulation
  
      def replace_shader_code(self, old, new):
          # TODO, will this work with VMobject structure, given
          # that it does not simpler return shader_wrappers of
          # family?
          for wrapper in self.get_shader_wrapper_list():
              wrapper.replace_code(old, new)
          return self
  
      def set_color_by_code(self, glsl_code):
          """
          Takes a snippet of code and inserts it into a
          context which has the following variables:
          vec4 color, vec3 point, vec3 unit_normal.
          The code should change the color variable
          """
          self.replace_shader_code("///// INSERT COLOR FUNCTION HERE /////", glsl_code)
          return self
  
      def set_color_by_xyz_func(
          self, glsl_snippet, min_value=-5.0, max_value=5.0, colormap="viridis"
      ):
          """
          Pass in a glsl expression in terms of x, y and z which returns
          a float.
          """
          # TODO, add a version of this which changes the point data instead
          # of the shader code
          for char in "xyz":
              glsl_snippet = glsl_snippet.replace(char, "point." + char)
          rgb_list = get_colormap_list(colormap)
          self.set_color_by_code(
              "color.rgb = float_to_color({}, {}, {}, {});".format(
                  glsl_snippet,
                  float(min_value),
                  float(max_value),
                  get_colormap_code(rgb_list),
              )
          )
          return self
  
      # For shader data
  
      # def refresh_shader_wrapper_id(self):
      #     self.shader_wrapper.refresh_id()
      #     return self
  
      def get_shader_wrapper(self):
          from ..renderer.shader_wrapper import ShaderWrapper
  
          self.shader_wrapper = ShaderWrapper(
              vert_data=self.get_shader_data(),
              vert_indices=self.get_shader_vert_indices(),
              uniforms=self.get_shader_uniforms(),
              depth_test=self.depth_test,
              texture_paths=self.texture_paths,
              render_primitive=self.render_primitive,
              shader_folder=self.__class__.shader_folder,
          )
          return self.shader_wrapper
  
      def get_shader_wrapper_list(self):
          shader_wrappers = it.chain(
              [self.get_shader_wrapper()],
              *[sm.get_shader_wrapper_list() for sm in self.submobjects],
          )
          batches = batch_by_property(shader_wrappers, lambda sw: sw.get_id())
  
          result = []
          for wrapper_group, _ in batches:
              shader_wrapper = wrapper_group[0]
              if not shader_wrapper.is_valid():
                  continue
              shader_wrapper.combine_with(*wrapper_group[1:])
              if len(shader_wrapper.vert_data) > 0:
                  result.append(shader_wrapper)
          return result
  
      def check_data_alignment(self, array, data_key):
          # Makes sure that self.data[key] can be broadcast into
          # the given array, meaning its length has to be either 1
          # 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(
                  self.data[data_key], len(array)
              )
          return self
  
      def get_resized_shader_data_array(self, length):
          # If possible, try to populate an existing array, rather
          # than recreating it each frame
          points = self.data["points"]
          shader_data = np.zeros(len(points), dtype=self.shader_dtype)
          return shader_data
  
      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]
  
      def get_shader_data(self):
          shader_data = self.get_resized_shader_data_array(self.get_num_points())
          self.read_data_to_shader(shader_data, "point", "points")
          return shader_data
  
      def refresh_shader_data(self):
          self.get_shader_data()
  
      def get_shader_uniforms(self):
          return self.uniforms
  
      def get_shader_vert_indices(self):
          return self.shader_indices
  
      # Event Handlers
      """
          Event handling follows the Event Bubbling model of DOM in javascript.
          Return false to stop the event bubbling.
          To learn more visit https://www.quirksmode.org/js/events_order.html
  
          Event Callback Argument is a callable function taking two arguments:
              1. Mobject
              2. EventData
      """
  
      def init_event_listners(self):
          self.event_listners = []
  
      def add_event_listner(self, event_type, event_callback):
          event_listner = EventListner(self, event_type, event_callback)
          self.event_listners.append(event_listner)
          EVENT_DISPATCHER.add_listner(event_listner)
          return self
  
      def remove_event_listner(self, event_type, event_callback):
          event_listner = EventListner(self, event_type, event_callback)
          while event_listner in self.event_listners:
              self.event_listners.remove(event_listner)
          EVENT_DISPATCHER.remove_listner(event_listner)
          return self
  
      def clear_event_listners(self, recurse=True):
          self.event_listners = []
          if recurse:
              for submob in self.submobjects:
                  submob.clear_event_listners(recurse=recurse)
          return self
  
      def get_event_listners(self):
          return self.event_listners
  
      def get_family_event_listners(self):
          return list(it.chain(*[sm.get_event_listners() for sm in self.get_family()]))
  
      def get_has_event_listner(self):
          return any(mob.get_event_listners() for mob in self.get_family())
  
      def add_mouse_motion_listner(self, callback):
          self.add_event_listner(EventType.MouseMotionEvent, callback)
  
      def remove_mouse_motion_listner(self, callback):
          self.remove_event_listner(EventType.MouseMotionEvent, callback)
  
      def add_mouse_press_listner(self, callback):
          self.add_event_listner(EventType.MousePressEvent, callback)
  
      def remove_mouse_press_listner(self, callback):
          self.remove_event_listner(EventType.MousePressEvent, callback)
  
      def add_mouse_release_listner(self, callback):
          self.add_event_listner(EventType.MouseReleaseEvent, callback)
  
      def remove_mouse_release_listner(self, callback):
          self.remove_event_listner(EventType.MouseReleaseEvent, callback)
  
      def add_mouse_drag_listner(self, callback):
          self.add_event_listner(EventType.MouseDragEvent, callback)
  
      def remove_mouse_drag_listner(self, callback):
          self.remove_event_listner(EventType.MouseDragEvent, callback)
  
      def add_mouse_scroll_listner(self, callback):
          self.add_event_listner(EventType.MouseScrollEvent, callback)
  
      def remove_mouse_scroll_listner(self, callback):
          self.remove_event_listner(EventType.MouseScrollEvent, callback)
  
      def add_key_press_listner(self, callback):
          self.add_event_listner(EventType.KeyPressEvent, callback)
  
      def remove_key_press_listner(self, callback):
          self.remove_event_listner(EventType.KeyPressEvent, callback)
  
      def add_key_release_listner(self, callback):
          self.add_event_listner(EventType.KeyReleaseEvent, callback)
  
      def remove_key_release_listner(self, callback):
          self.remove_event_listner(EventType.KeyReleaseEvent, callback)
  
      # Errors
  
      def throw_error_if_no_points(self):
          if not self.has_points():
              message = "Cannot call Mobject.{} " + "for a Mobject with no points"
              caller_name = sys._getframe(1).f_code.co_name
              raise Exception(message.format(caller_name))
  
  
  class OpenGLGroup(OpenGLMobject):
      def __init__(self, *mobjects, **kwargs):
          if not all([isinstance(m, OpenGLMobject) for m in mobjects]):
              raise Exception("All submobjects must be of type Mobject")
          OpenGLMobject.__init__(self, **kwargs)
          self.add(*mobjects)
  
  
  class OpenGLPoint(OpenGLMobject):
      def __init__(
          self, location=ORIGIN, artificial_width=1e-6, artificial_height=1e-6, **kwargs
      ):
          self.artificial_width = artificial_width
          self.artificial_height = artificial_height
          OpenGLMobject.__init__(self, **kwargs)
          self.set_location(location)
  
      def get_width(self):
          return self.artificial_width
  
      def get_height(self):
          return self.artificial_height
  
      def get_location(self):
          return self.get_points()[0].copy()
  
      def get_bounding_box_point(self, *args, **kwargs):
          return self.get_location()
  
      def set_location(self, new_loc):
          self.set_points(np.array(new_loc, ndmin=2, dtype=float))
  
  
  class _AnimationBuilder:
      def __init__(self, mobject):
          self.mobject = mobject
          self.overridden_animation = None
          self.mobject.generate_target()
          self.is_chaining = False
          self.methods = []
  
      def __getattr__(self, method_name):
          method = getattr(self.mobject.target, method_name)
          self.methods.append(method)
          has_overridden_animation = hasattr(method, "_override_animate")
  
          if (self.is_chaining and has_overridden_animation) or self.overridden_animation:
              raise NotImplementedError(
                  "Method chaining is currently not supported for "
                  "overridden animations"
              )
  
          def update_target(*method_args, **method_kwargs):
              if has_overridden_animation:
                  self.overridden_animation = method._override_animate(
                      self.mobject, *method_args, **method_kwargs
                  )
              else:
                  method(*method_args, **method_kwargs)
              return self
  
          self.is_chaining = True
          return update_target
  
      def build(self):
          from ..animation.transform import _MethodAnimation
  
          if self.overridden_animation:
              return self.overridden_animation
  
          return _MethodAnimation(self.mobject, self.methods)
  
  
  def override_animate(method):
      def decorator(animation_method):
          method._override_animate = animation_method
          return animation_method
  
      return decorator
  