3b1b-manim/manimlib/mobject/mobject.py

1268 lines
41 KiB
Python
Raw Normal View History

from functools import reduce
import copy
import itertools as it
2015-10-01 17:20:17 -07:00
import operator as op
2015-06-10 22:00:35 -07:00
import os
2018-06-18 13:27:42 -07:00
import random
2019-03-20 13:07:08 -07:00
import sys
2020-02-13 10:39:26 -08:00
import moderngl
2015-06-10 22:00:35 -07:00
from colour import Color
import numpy as np
2015-06-10 22:00:35 -07:00
import manimlib.constants as consts
from manimlib.constants import *
from manimlib.container.container import Container
from manimlib.utils.color import color_gradient
from manimlib.utils.color import interpolate_color
from manimlib.utils.iterables import batch_by_property
from manimlib.utils.iterables import list_update
from manimlib.utils.bezier import interpolate
from manimlib.utils.paths import straight_path
from manimlib.utils.simple_functions import get_parameters
from manimlib.utils.space_ops import angle_of_vector
from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import rotation_matrix_transpose
from manimlib.utils.shaders import get_shader_info
from manimlib.utils.shaders import shader_info_to_id
from manimlib.utils.shaders import shader_id_to_info
from manimlib.utils.shaders import is_valid_shader_info
2015-06-10 22:00:35 -07:00
# TODO: Explain array_attrs
2020-02-11 19:55:00 -08:00
# TODO: Incorporate shader defaults
2016-04-09 20:03:57 -07:00
class Mobject(Container):
2015-06-10 22:00:35 -07:00
"""
Mathematical Object
"""
2016-02-27 16:32:53 -08:00
CONFIG = {
"color": WHITE,
"name": None,
"dim": 3,
# 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,
2020-02-13 10:39:26 -08:00
# For shaders
"vert_shader_file": "",
"geom_shader_file": "",
"frag_shader_file": "",
"render_primative": moderngl.TRIANGLE_STRIP,
"texture_path": "",
# Must match in attributes of vert shader
"shader_dtype": [
('point', np.float32, (3,)),
]
}
2019-02-06 21:16:26 -08:00
def __init__(self, **kwargs):
Container.__init__(self, **kwargs)
self.submobjects = []
self.parents = []
self.family = [self]
if self.name is None:
self.name = self.__class__.__name__
self.time_based_updaters = []
self.non_time_updaters = []
self.updating_suspended = False
self.shader_data_is_locked = False
2018-08-20 15:49:31 -07:00
self.reset_points()
self.init_points()
2016-04-17 19:29:27 -07:00
self.init_colors()
self.init_shader_data()
2015-09-24 10:54:59 -07:00
def __str__(self):
2016-09-16 14:06:44 -07:00
return str(self.name)
2018-08-20 15:49:31 -07:00
def reset_points(self):
2016-04-09 20:03:57 -07:00
self.points = np.zeros((0, self.dim))
def init_colors(self):
# For subclasses
2016-04-09 20:03:57 -07:00
pass
2015-06-10 22:00:35 -07:00
2020-02-11 19:55:00 -08:00
def init_points(self):
# Typically implemented in subclass, unless purposefully left blank
pass
2015-06-10 22:00:35 -07:00
# Family matters
def __getitem__(self, value):
self_list = self.split()
if isinstance(value, slice):
GroupClass = self.get_group_class()
return GroupClass(*self_list.__getitem__(value))
return self_list.__getitem__(value)
def __iter__(self):
return iter(self.split())
def __len__(self):
return len(self.split())
def split(self):
result = [self] if len(self.points) > 0 else []
return result + self.submobjects
def assemble_family(self):
sub_families = [sm.get_family() for sm in self.submobjects]
self.family = [self, *it.chain(*sub_families)]
for parent in self.parents:
parent.assemble_family()
return self
def get_family(self):
return self.family
def family_members_with_points(self):
return [m for m in self.get_family() if m.get_num_points() > 0]
def add(self, *mobjects):
2017-09-28 07:40:57 -07:00
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()
2016-08-09 14:07:23 -07:00
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):
2020-02-23 22:58:47 +00:00
self.set_submobjects(list_update(mobjects, self.submobjects))
return self
2020-02-22 13:19:51 -08:00
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)
2016-09-01 11:11:57 -07:00
return self
def get_array_attrs(self):
# May be more for other Mobject types
2016-04-09 20:03:57 -07:00
return ["points"]
def digest_mobject_attrs(self):
"""
Ensures all attributes which are mobjects are included
2016-04-17 00:31:38 -07:00
in the submobjects list.
"""
2018-08-09 17:56:05 -07:00
mobject_attrs = [x for x in list(self.__dict__.values()) if isinstance(x, Mobject)]
self.set_submobjects(list_update(self.submobjects, mobject_attrs))
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
# Displaying
def get_image(self, camera=None):
# TODO, this doesn't...you know, seem to actually work
camera.clear()
camera.capture(self)
2017-09-26 17:41:45 -07:00
return camera.get_image()
2016-04-09 20:03:57 -07:00
def show(self, camera):
self.get_image(camera).show()
def save_image(self, name=None):
2016-04-09 20:03:57 -07:00
self.get_image().save(
os.path.join(consts.VIDEO_DIR, (name or str(self)) + ".png")
)
def copy(self):
# TODO, either justify reason for shallow copy, or
# remove this redundancy everywhere
2018-04-16 14:17:34 -07:00
# return self.deepcopy()
2018-02-07 07:35:18 +01:00
2020-02-27 17:19:15 +00:00
parents = self.parents
self.parents = []
copy_mobject = copy.copy(self)
2020-02-27 17:19:15 +00:00
self.parents = parents
copy_mobject.points = np.array(self.points)
copy_mobject.submobjects = []
copy_mobject.add(*[sm.copy() for sm in self.submobjects])
copy_mobject.match_updaters(self)
# Make sure any mobject or numpy array attributes are copied
2018-08-21 19:15:16 -07:00
family = self.get_family()
2018-08-09 17:56:05 -07:00
for attr, value in list(self.__dict__.items()):
if isinstance(value, Mobject) and value in family and value is not self:
setattr(copy_mobject, attr, value.copy())
2018-08-11 16:53:37 -07:00
if isinstance(value, np.ndarray):
setattr(copy_mobject, attr, np.array(value))
return copy_mobject
2017-05-12 17:08:49 +02:00
def deepcopy(self):
2020-02-27 17:19:15 +00:00
parents = self.parents
self.parents = []
2020-03-06 06:16:52 -08:00
result = copy.deepcopy(self)
2020-02-27 17:19:15 +00:00
self.parents = parents
2020-03-06 06:16:52 -08:00
return result
def generate_target(self, use_deepcopy=False):
self.target = None # Prevent exponential explosion
2017-10-05 10:07:28 -07:00
if use_deepcopy:
self.target = self.deepcopy()
else:
self.target = self.copy()
2016-09-16 14:06:44 -07:00
return self.target
# Updating
def update(self, dt=0, recursive=True):
2019-02-03 12:11:56 -08:00
if 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 recursive:
for submob in self.submobjects:
submob.update(dt, recursive)
return self
def get_time_based_updaters(self):
return self.time_based_updaters
2019-02-15 20:05:45 -08:00
def has_time_based_updater(self):
return len(self.time_based_updaters) > 0
2019-02-15 20:05:45 -08:00
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()
]))
2018-08-30 14:24:25 -07:00
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
2018-08-30 14:24:25 -07:00
if index is None:
updater_list.append(update_function)
2018-08-30 14:24:25 -07:00
else:
updater_list.insert(index, update_function)
if call_updater:
self.update(0)
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)
return self
2019-02-03 12:11:56 -08:00
def clear_updaters(self, recursive=True):
self.time_based_updaters = []
self.non_time_updaters = []
2019-02-03 12:11:56 -08:00
if recursive:
for submob in self.submobjects:
submob.clear_updaters()
return self
2019-04-10 14:57:15 -07:00
def match_updaters(self, mobject):
self.clear_updaters()
for updater in mobject.get_updaters():
self.add_updater(updater)
return self
def suspend_updating(self, recursive=True):
self.updating_suspended = True
if recursive:
for submob in self.submobjects:
submob.suspend_updating(recursive)
return self
def resume_updating(self, recursive=True):
self.updating_suspended = False
if recursive:
for submob in self.submobjects:
submob.resume_updating(recursive)
self.update(dt=0, recursive=recursive)
return self
2018-04-09 13:47:46 -07:00
# Transforming operations
2020-02-18 22:32:43 -08:00
def set_points(self, points):
self.points = np.array(points)
return self
2016-04-09 20:03:57 -07:00
def apply_to_family(self, func):
for mob in self.family_members_with_points():
2016-04-09 20:03:57 -07:00
func(mob)
def shift(self, *vectors):
total_vector = reduce(op.add, vectors)
for mob in self.get_family():
mob.points += total_vector
2017-05-12 17:08:49 +02:00
return self
2016-04-09 20:03:57 -07:00
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
2018-04-09 13:47:46 -07:00
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_about_point(
lambda points: scale_factor * points,
**kwargs
)
return self
def rotate_about_origin(self, angle, axis=OUT, axes=[]):
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_about_point(
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 stretch(self, factor, dim, **kwargs):
def func(points):
points[:, dim] *= factor
return points
self.apply_points_function_about_point(func, **kwargs)
return self
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_about_point(
2020-02-19 23:43:33 -08:00
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
2018-08-28 09:44:10 -07:00
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_about_point(
lambda points: np.dot(points, full_matrix.T),
**kwargs
)
2017-08-24 19:06:02 -07:00
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.points, np.transpose(axis))
alphas -= min(alphas)
alphas /= max(alphas)
alphas = alphas**wag_factor
mob.points += np.dot(
alphas.reshape((len(alphas), 1)),
np.array(direction).reshape((1, mob.dim))
)
return self
2016-01-04 10:15:57 -08:00
def reverse_points(self):
for mob in self.family_members_with_points():
mob.apply_over_attr_arrays(lambda arr: arr[::-1])
2016-01-04 10:15:57 -08:00
return self
def repeat(self, count):
"""
This can make transition animations nicer
"""
for mob in self.family_members_with_points():
mob.apply_over_attr_arrays(lambda arr: np.vstack([arr] * count))
return self
2018-04-09 13:47:46 -07:00
# In place operations.
# Note, much of these are now redundant with default behavior of
# above methods
def apply_points_function_about_point(self, func, about_point=None, about_edge=None):
if about_point is None:
if about_edge is None:
about_edge = ORIGIN
about_point = self.get_bounding_box_point(about_edge)
2020-02-20 15:52:23 -08:00
for mob in self.family_members_with_points():
mob.points -= about_point
mob.points[:] = func(mob.points)
mob.points += about_point
return self
def rotate_in_place(self, angle, axis=OUT):
# redundant with default behavior of rotate now.
return self.rotate(angle, axis=axis)
2015-06-10 22:00:35 -07:00
def scale_in_place(self, scale_factor, **kwargs):
# Redundant with default behavior of scale now.
return self.scale(scale_factor, **kwargs)
2015-06-10 22:00:35 -07:00
def scale_about_point(self, scale_factor, point):
# Redundant with default behavior of scale now.
return self.scale(scale_factor, about_point=point)
def pose_at_angle(self, angle=TAU / 14, axis=UR, **kwargs):
return self.rotate(angle, axis, **kwargs)
2015-06-13 19:00:23 -07:00
2018-04-09 13:47:46 -07:00
# Positioning methods
2015-06-10 22:00:35 -07:00
def center(self):
self.shift(-self.get_center())
return self
def align_on_border(self, direction, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER):
2015-06-19 08:31:02 -07:00
"""
Direction just needs to be a vector pointing towards side or
corner in the 2d plane.
"""
target_point = np.sign(direction) * (FRAME_X_RADIUS, FRAME_Y_RADIUS, 0)
point_to_align = self.get_bounding_box_point(direction)
2016-07-25 16:04:54 -07:00
shift_val = target_point - point_to_align - buff * np.array(direction)
2016-01-04 10:15:57 -08:00
shift_val = shift_val * abs(np.sign(direction))
self.shift(shift_val)
2015-06-19 08:31:02 -07:00
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)
2017-05-12 17:08:49 +02:00
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]),
2016-08-20 20:23:01 -07:00
):
2016-07-25 16:04:54 -07:00
if isinstance(mobject_or_point, Mobject):
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
2016-08-20 20:23:01 -07:00
)
2016-07-25 16:04:54 -07:00
else:
target_point = mobject_or_point
2018-01-16 22:38:00 -08:00
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)
2015-09-24 10:54:59 -07:00
return self
2016-08-02 12:26:15 -07:00
def shift_onto_screen(self, **kwargs):
space_lengths = [FRAME_X_RADIUS, FRAME_Y_RADIUS]
for vect in UP, DOWN, LEFT, RIGHT:
dim = np.argmax(np.abs(vect))
2017-01-25 16:40:59 -08:00
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_EDGE_BUFFER)
max_val = space_lengths[dim] - buff
2017-01-27 19:31:20 -08:00
edge_center = self.get_edge_center(vect)
if np.dot(edge_center, vect) > max_val:
2016-08-02 12:26:15 -07:00
self.to_edge(vect, **kwargs)
2016-09-16 14:06:44 -07:00
return self
2017-01-16 13:26:46 -08:00
def is_off_screen(self):
if self.get_left()[0] > FRAME_X_RADIUS:
2017-01-16 13:26:46 -08:00
return True
if self.get_right()[0] < -FRAME_X_RADIUS:
2017-01-16 13:26:46 -08:00
return True
if self.get_bottom()[1] > FRAME_Y_RADIUS:
2017-01-16 13:26:46 -08:00
return True
if self.get_top()[1] < -FRAME_Y_RADIUS:
2017-01-16 13:26:46 -08:00
return True
return False
def stretch_about_point(self, factor, dim, point):
return self.stretch(factor, dim, about_point=point)
2016-09-24 22:46:17 -07:00
def stretch_in_place(self, factor, dim):
# Now redundant with stretch
return self.stretch(factor, dim)
2016-09-24 22:46:17 -07:00
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)
2018-02-27 13:55:38 -08:00
def stretch_to_fit_depth(self, depth, **kwargs):
return self.rescale_to_fit(depth, 1, stretch=True, **kwargs)
2018-02-27 13:55:38 -08:00
def set_width(self, width, stretch=False, **kwargs):
return self.rescale_to_fit(width, 0, stretch=stretch, **kwargs)
2015-10-20 21:55:46 -07:00
def set_height(self, height, stretch=False, **kwargs):
return self.rescale_to_fit(height, 1, stretch=stretch, **kwargs)
def set_depth(self, depth, stretch=False, **kwargs):
return self.rescale_to_fit(depth, 2, stretch=stretch, **kwargs)
2017-09-06 20:18:19 -07:00
2019-02-14 11:34:06 -08:00
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)
2017-01-26 19:59:55 -08:00
for submob in self.submobjects:
submob.scale(1. / factor)
2017-01-26 19:59:55 -08:00
return self
def move_to(self, point_or_mobject, aligned_edge=ORIGIN,
coor_mask=np.array([1, 1, 1])):
if isinstance(point_or_mobject, Mobject):
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
2015-10-20 21:55:46 -07:00
def replace(self, mobject, dim_to_match=0, stretch=False):
2016-04-17 00:31:38 -07:00
if not mobject.get_num_points() and not mobject.submobjects:
raise Warning("Attempting to replace mobject with no points")
return self
2015-08-03 22:23:00 -07:00
if stretch:
self.stretch_to_fit_width(mobject.get_width())
self.stretch_to_fit_height(mobject.get_height())
else:
self.rescale_to_fit(
2016-08-13 18:27:02 -07:00
mobject.length_over_dim(dim_to_match),
2017-05-12 17:08:49 +02:00
dim_to_match,
stretch=False
2016-08-13 18:27:02 -07:00
)
self.shift(mobject.get_center() - self.get_center())
return self
2019-02-07 09:26:18 -08:00
def surround(self, mobject,
dim_to_match=0,
stretch=False,
buff=MED_SMALL_BUFF):
self.replace(mobject, dim_to_match, stretch)
2019-02-07 09:26:18 -08:00
length = mobject.length_over_dim(dim_to_match)
self.scale_in_place((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
2016-02-21 15:44:54 -08:00
if np.all(curr_vect == 0):
raise Exception("Cannot position endpoints of closed loop")
target_vect = end - start
2019-02-07 21:14:30 -08:00
self.scale(
get_norm(target_vect) / get_norm(curr_vect),
about_point=curr_start,
)
2016-02-21 15:44:54 -08:00
self.rotate(
angle_of_vector(target_vect) -
2019-02-07 21:14:30 -08:00
angle_of_vector(curr_vect),
about_point=curr_start
2016-02-21 15:44:54 -08:00
)
2019-02-07 21:14:30 -08:00
self.shift(start - curr_start)
2016-02-21 15:44:54 -08:00
return self
# Background rectangle
def add_background_rectangle(self, color=BLACK, opacity=0.75, **kwargs):
# TODO, this does not behave well when the mobject has points,
# since it gets displayed on top
2018-12-27 19:12:52 +08:00
from manimlib.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
# Color functions
2016-02-21 15:44:54 -08:00
2020-06-07 12:23:35 -07:00
def set_color(self, color, family=True):
# Here it just recurses to submobjects, but in subclasses this
# should be further implemented based on the the inner workings
# of color
if family:
for submob in self.submobjects:
submob.set_color(color, family=family)
return self
2016-04-09 20:03:57 -07:00
2020-06-07 12:23:35 -07:00
def set_opacity(self, opacity, family=True):
# Here it just recurses to submobjects, but in subclasses this
# should be further implemented based on the the inner workings
# of color
if family:
for submob in self.submobjects:
submob.set_opacity(opacity, family=family)
return self
def get_color(self):
raise Exception("Not implemented")
def get_opacity(self):
raise Exception("Not implemented")
def set_color_by_gradient(self, *colors):
self.set_submobject_colors_by_gradient(*colors)
2016-08-19 15:54:16 -07:00
return self
2016-07-22 11:22:31 -07:00
def set_colors_by_radial_gradient(self, center=None, radius=1, inner_color=WHITE, outer_color=BLACK):
self.set_submobject_colors_by_radial_gradient(
center, radius, inner_color, outer_color)
2018-01-17 15:18:02 -08:00
return self
def set_submobject_colors_by_gradient(self, *colors):
2016-08-09 14:07:23 -07:00
if len(colors) == 0:
raise Exception("Need at least one color")
elif len(colors) == 1:
2018-03-30 11:51:31 -07:00
return self.set_color(*colors)
2016-08-09 14:07:23 -07:00
2016-07-22 11:22:31 -07:00
mobs = self.family_members_with_points()
2016-10-25 17:35:16 -07:00
new_colors = color_gradient(colors, len(mobs))
2018-01-17 15:18:02 -08:00
2016-08-09 14:07:23 -07:00
for mob, color in zip(mobs, new_colors):
mob.set_color(color, family=False)
2016-07-22 11:22:31 -07:00
return self
2015-06-10 22:00:35 -07:00
def set_submobject_colors_by_radial_gradient(self, center=None, radius=1, inner_color=WHITE, outer_color=BLACK):
2018-04-09 13:47:46 -07:00
if center is None:
2018-01-17 15:18:02 -08:00
center = self.get_center()
for mob in self.family_members_with_points():
2018-08-15 17:30:24 -07:00
t = get_norm(mob.get_center() - center) / radius
t = min(t, 1)
2018-01-17 15:18:02 -08:00
mob_color = interpolate_color(inner_color, outer_color, t)
mob.set_color(mob_color, family=False)
2018-01-17 15:18:02 -08:00
return self
2019-02-11 21:15:15 -08:00
def fade_to(self, color, alpha, family=True):
if self.get_num_points() > 0:
2019-02-11 21:15:15 -08:00
new_color = interpolate_color(
self.get_color(), color, alpha
)
self.set_color(new_color, family=False)
if family:
for submob in self.submobjects:
submob.fade_to(color, alpha)
return self
2019-02-11 21:15:15 -08:00
def fade(self, darkness=0.5, family=True):
if family:
for submob in self.submobjects:
submob.fade(darkness, family)
2015-06-10 22:00:35 -07:00
return self
def get_gloss(self):
return self.gloss
def set_gloss(self, gloss, family=True):
self.gloss = gloss
if family:
for submob in self.submobjects:
submob.set_gloss(gloss, family)
return self
def get_shadow(self):
return self.shadow
def set_shadow(self, shadow, family=True):
self.shadow = shadow
if family:
for submob in self.submobjects:
submob.set_shadow(shadow, family)
return self
2016-04-09 20:03:57 -07:00
##
def save_state(self, use_deepcopy=False):
2016-11-15 20:08:28 -08:00
if hasattr(self, "saved_state"):
# Prevent exponential growth of data
2016-11-15 20:08:28 -08:00
self.saved_state = None
if use_deepcopy:
self.saved_state = self.deepcopy()
else:
self.saved_state = self.copy()
2016-08-09 14:07:23 -07:00
return self
def restore(self):
2017-02-27 15:56:22 -08:00
if not hasattr(self, "saved_state") or self.save_state is None:
2016-08-09 14:07:23 -07:00
raise Exception("Trying to restore without having saved")
self.become(self.saved_state)
2016-08-09 14:07:23 -07:00
return self
2016-04-09 20:03:57 -07:00
##
2016-04-09 20:03:57 -07:00
def get_merged_array(self, array_attr):
if self.submobjects:
return np.vstack([
getattr(sm, array_attr)
for sm in self.get_family()
])
else:
return getattr(self, array_attr)
def get_all_points(self):
if self.submobjects:
return np.vstack([
sm.points for sm in self.get_family()
])
else:
return self.points
2018-04-09 13:47:46 -07:00
# Getters
2015-06-13 19:00:23 -07:00
2016-12-26 07:10:38 -08:00
def get_points_defining_boundary(self):
return self.get_all_points()
2016-12-26 07:10:38 -08:00
2016-04-14 19:30:47 -07:00
def get_num_points(self):
return len(self.points)
def get_bounding_box_point(self, direction):
result = np.zeros(self.dim)
bb = self.get_bounding_box()
result[direction < 0] = bb[0, direction < 0]
result[direction == 0] = bb[1, direction == 0]
result[direction > 0] = bb[2, direction > 0]
return result
def get_bounding_box(self):
all_points = self.get_points_defining_boundary()
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])
# Pseudonyms for more general get_bounding_box_point method
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)
2015-06-13 19:00:23 -07:00
2015-06-10 22:00:35 -07:00
def get_center(self):
return self.get_bounding_box_point(np.zeros(self.dim))
def get_center_of_mass(self):
2020-02-19 23:43:33 -08:00
return self.get_all_points().mean(0)
2015-06-10 22:00:35 -07:00
def get_boundary_point(self, direction):
all_points = self.get_points_defining_boundary()
index = np.argmax(np.dot(all_points, np.array(direction).T))
return all_points[index]
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)
2017-08-27 14:43:18 -07:00
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 (bb[2] - bb[0])[dim]
2015-06-10 22:00:35 -07:00
def get_width(self):
return self.length_over_dim(0)
2015-06-10 22:00:35 -07:00
def get_height(self):
return self.length_over_dim(1)
2017-08-27 14:43:18 -07:00
def get_depth(self):
return self.length_over_dim(2)
2019-02-14 11:34:06 -08:00
def get_coord(self, dim, direction=ORIGIN):
"""
Meant to generalize get_x, get_y, get_z
"""
return self.get_bounding_box_point(direction)[dim]
2019-02-14 11:34:06 -08:00
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.points[0])
def get_end(self):
self.throw_error_if_no_points()
return np.array(self.points[-1])
def get_start_and_end(self):
return self.get_start(), self.get_end()
2016-02-15 23:43:28 -08:00
def point_from_proportion(self, alpha):
2016-04-09 20:03:57 -07:00
raise Exception("Not implemented")
2015-06-10 22:00:35 -07:00
def pfp(self, alpha):
"""Abbreviation fo point_from_proportion"""
return self.point_from_proportion(alpha)
2018-08-20 15:49:31 -07:00
def get_pieces(self, n_pieces):
template = self.copy()
template.set_submobjects([])
2018-08-20 15:49:31 -07:00
alphas = np.linspace(0, 1, n_pieces + 1)
return Group(*[
template.copy().pointwise_become_partial(
self, a1, a2
)
for a1, a2 in zip(alphas[:-1], alphas[1:])
])
2019-02-14 11:34:06 -08:00
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()
2019-02-11 22:24:02 -08:00
def has_points(self):
return len(self.points) > 0
def has_no_points(self):
return not self.has_points()
2019-02-14 11:34:06 -08:00
# 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, alignment_vect=UP):
"""
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, Mobject):
point = mobject_or_point.get_bounding_box_point(direction)
2019-02-14 11:34:06 -08:00
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 Group
# Submobject organization
2019-02-04 14:54:25 -08:00
def arrange(self, direction=RIGHT, center=True, **kwargs):
2016-05-03 23:14:40 -07:00
for m1, m2 in zip(self.submobjects, self.submobjects[1:]):
m2.next_to(m1, direction, **kwargs)
2016-05-03 23:14:40 -07:00
if center:
self.center()
return self
2016-04-09 20:03:57 -07:00
2019-02-04 14:54:25 -08:00
def arrange_in_grid(self, n_rows=None, n_cols=None, **kwargs):
2017-08-27 14:43:18 -07:00
submobs = self.submobjects
if n_rows is None and n_cols is None:
n_rows = int(np.sqrt(len(submobs)))
2017-08-27 14:43:18 -07:00
if n_rows is not None:
v1 = RIGHT
v2 = DOWN
n = len(submobs) // n_rows
2017-08-27 14:43:18 -07:00
elif n_cols is not None:
v1 = DOWN
v2 = RIGHT
n = len(submobs) // n_cols
2017-08-27 14:43:18 -07:00
Group(*[
2019-02-04 14:54:25 -08:00
Group(*submobs[i:i + n]).arrange(v1, **kwargs)
2017-08-27 14:43:18 -07:00
for i in range(0, len(submobs), n)
2019-02-04 14:54:25 -08:00
]).arrange(v2, **kwargs)
2017-08-27 14:43:18 -07:00
return self
def sort(self, point_to_num_func=lambda p: p[0], submob_func=None):
if submob_func is None:
submob_func = lambda m: point_to_num_func(m.get_center())
self.submobjects.sort(key=submob_func)
return self
def shuffle(self, recursive=False):
2018-06-18 13:27:42 -07:00
if recursive:
for submob in self.submobjects:
submob.shuffle(recursive=True)
2018-06-18 13:27:42 -07:00
random.shuffle(self.submobjects)
# Just here to keep from breaking old scenes.
def arrange_submobjects(self, *args, **kwargs):
return self.arrange(*args, **kwargs)
def sort_submobjects(self, *args, **kwargs):
return self.sort(*args, **kwargs)
def shuffle_submobjects(self, *args, **kwargs):
return self.shuffle(*args, **kwargs)
# Alignment
def align_data(self, mobject):
self.null_point_align(mobject) # Needed?
2016-04-17 00:31:38 -07:00
self.align_submobjects(mobject)
for mob1, mob2 in zip(self.get_family(), mobject.get_family()):
mob1.align_points(mob2)
def align_points(self, mobject):
count1 = self.get_num_points()
count2 = mobject.get_num_points()
if count1 < count2:
self.align_points_with_larger(mobject)
elif count2 < count1:
mobject.align_points_with_larger(self)
return self
def align_points_with_larger(self, larger_mobject):
raise Exception("Not implemented")
2016-04-17 00:31:38 -07:00
def align_submobjects(self, mobject):
mob1 = self
mob2 = mobject
n1 = len(mob1.submobjects)
n2 = len(mob2.submobjects)
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_submobjects(sm2)
2016-04-14 19:30:47 -07:00
return self
2016-04-23 23:36:05 -07:00
def null_point_align(self, mobject):
"""
2019-02-11 22:24:02 -08:00
If a mobject with points is being aligned to
one without, treat both as groups, and push
the one with points into its own submobjects
list.
2016-04-23 23:36:05 -07:00
"""
2019-02-11 22:24:02 -08:00
for m1, m2 in (self, mobject), (mobject, self):
if m1.has_no_points() and m2.has_points():
m2.push_self_into_submobjects()
2016-04-23 23:36:05 -07:00
return self
2016-04-17 00:31:38 -07:00
def push_self_into_submobjects(self):
2020-02-20 15:52:23 -08:00
copy = self.deepcopy()
2020-02-22 13:19:51 -08:00
copy.set_submobjects([])
2018-08-20 15:49:31 -07:00
self.reset_points()
2016-04-14 19:30:47 -07:00
self.add(copy)
return self
2016-04-17 00:31:38 -07:00
def add_n_more_submobjects(self, n):
if n == 0:
return
2016-04-17 12:59:53 -07:00
curr = len(self.submobjects)
if curr == 0:
# If empty, simply add n point mobjects
self.set_submobjects([
2020-02-20 15:52:23 -08:00
self.copy().scale(0)
for k in range(n)
])
return
target = curr + n
repeat_indices = (np.arange(target) * curr) // target
split_factors = [
2020-02-20 15:52:23 -08:00
(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 k in range(1, sf):
2020-02-20 15:52:23 -08:00
new_submobs.append(submob.copy().fade(1))
self.set_submobjects(new_submobs)
2016-04-14 19:30:47 -07:00
return self
def interpolate(self, mobject1, mobject2, alpha, path_func=straight_path):
2015-06-10 22:00:35 -07:00
"""
2017-05-12 17:08:49 +02:00
Turns self into an interpolation between mobject1
2015-06-10 22:00:35 -07:00
and mobject2.
"""
self.points[:] = path_func(mobject1.points, mobject2.points, alpha)
self.interpolate_color(mobject1, mobject2, alpha)
self.interpolate_light_style(mobject1, mobject2, alpha)
2019-06-15 12:22:21 -07:00
return self
def interpolate_color(self, mobject1, mobject2, alpha):
pass # To implement in subclass
2015-06-10 22:00:35 -07:00
def interpolate_light_style(self, mobject1, mobject2, alpha):
self.set_gloss(interpolate(mobject1.get_gloss(), mobject2.get_gloss(), alpha))
self.set_shadow(interpolate(mobject1.get_shadow(), mobject2.get_shadow(), alpha))
return self
def become_partial(self, mobject, a, b):
"""
Set points in such a way as to become only
2017-05-12 17:08:49 +02:00
part of mobject.
Inputs 0 <= a < b <= 1 determine what portion
of mobject to become.
"""
pass # To implement in subclasses
2016-09-16 14:06:44 -07:00
# TODO, color?
def pointwise_become_partial(self, mobject, a, b):
pass # To implement in subclass
2020-02-18 22:32:43 -08:00
def become(self, mobject):
2018-08-09 11:09:24 -07:00
"""
Edit points, colors and submobjects to be idential
to another mobject
"""
self.align_submobjects(mobject)
2018-08-26 13:17:23 -07:00
for sm1, sm2 in zip(self.get_family(), mobject.get_family()):
2020-02-18 22:32:43 -08:00
sm1.set_points(sm2.points)
2018-08-30 14:24:25 -07:00
sm1.interpolate_color(sm1, sm2, 1)
return self
2018-08-09 11:09:24 -07:00
2020-02-11 19:55:00 -08:00
def prepare_for_animation(self):
pass
def cleanup_from_animation(self):
pass
2020-02-13 10:39:26 -08:00
# For shaders
def init_shader_data(self):
self.shader_data = np.zeros(len(self.points), dtype=self.shader_dtype)
def get_blank_shader_data_array(self, size, name="shader_data"):
# If possible, try to populate an existing array, rather
# than recreating it each frame
arr = getattr(self, name)
if arr.size != size:
new_arr = np.resize(arr, size)
setattr(self, name, new_arr)
return new_arr
return arr
2020-02-13 10:39:26 -08:00
def lock_shader_data(self):
self.shader_data_is_locked = False
self.saved_shader_info_list = self.get_shader_info_list()
self.shader_data_is_locked = True
def unlock_shader_data(self):
self.shader_data_is_locked = False
2020-02-13 10:39:26 -08:00
def get_shader_info_list(self):
2020-02-21 12:00:34 -08:00
if self.shader_data_is_locked:
return self.saved_shader_info_list
shader_infos = it.chain(
[self.get_shader_info()],
*[
submob.get_shader_info_list()
for submob in self.submobjects
]
)
batches = batch_by_property(shader_infos, shader_info_to_id)
result = []
for info_group, sid in batches:
shader_info = shader_id_to_info(sid)
shader_info["data"] = np.hstack([info["data"] for info in info_group])
if is_valid_shader_info(shader_info):
result.append(shader_info)
return result
2020-02-13 10:39:26 -08:00
def get_shader_info(self):
return get_shader_info(
data=self.get_shader_data(),
vert_file=self.vert_shader_file,
geom_file=self.geom_shader_file,
frag_file=self.frag_shader_file,
texture_path=self.texture_path,
render_primative=self.render_primative,
)
2020-02-13 10:39:26 -08:00
def get_shader_data(self):
# Typically to be implemented by subclasses
2020-02-13 10:39:26 -08:00
# Must return a structured numpy array
return self.shader_data
2020-02-13 10:39:26 -08:00
# Errors
def throw_error_if_no_points(self):
if self.has_no_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 Group(Mobject):
2019-02-06 21:16:26 -08:00
def __init__(self, *mobjects, **kwargs):
if not all([isinstance(m, Mobject) for m in mobjects]):
raise Exception("All submobjects must be of type Mobject")
Mobject.__init__(self, **kwargs)
self.add(*mobjects)
2020-02-14 15:29:52 -08:00
class Point(Mobject):
CONFIG = {
"artificial_width": 1e-6,
"artificial_height": 1e-6,
}
def __init__(self, location=ORIGIN, **kwargs):
Mobject.__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 np.array(self.points[0])
def get_bounding_box_point(self, *args, **kwargs):
return self.get_location()
def set_location(self, new_loc):
self.points = np.array(new_loc, ndmin=2, dtype=float)