2015-06-10 22:00:35 -07:00
|
|
|
import numpy as np
|
|
|
|
import itertools as it
|
|
|
|
import os
|
|
|
|
from PIL import Image
|
|
|
|
from random import random
|
|
|
|
from copy import deepcopy
|
|
|
|
from colour import Color
|
2015-08-17 11:12:56 -07:00
|
|
|
import inspect
|
2015-06-10 22:00:35 -07:00
|
|
|
|
|
|
|
from constants import *
|
|
|
|
from helpers import *
|
|
|
|
import displayer as disp
|
|
|
|
|
|
|
|
|
|
|
|
class Mobject(object):
|
|
|
|
"""
|
|
|
|
Mathematical Object
|
|
|
|
"""
|
|
|
|
#Number of numbers used to describe a point (3 for pos, 3 for normal vector)
|
|
|
|
DIM = 3
|
|
|
|
DEFAULT_COLOR = Color("skyblue")
|
|
|
|
SHOULD_BUFF_POINTS = GENERALLY_BUFF_POINTS
|
2015-08-17 11:12:56 -07:00
|
|
|
EDGE_BUFFER = 0.5
|
2015-06-10 22:00:35 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
color = None,
|
|
|
|
name = None,
|
|
|
|
center = None,
|
2015-08-01 11:34:33 -07:00
|
|
|
**kwargs
|
2015-06-10 22:00:35 -07:00
|
|
|
):
|
|
|
|
self.color = Color(color) if color else Color(self.DEFAULT_COLOR)
|
|
|
|
if not hasattr(self, "name"):
|
|
|
|
self.name = name or self.__class__.__name__
|
|
|
|
self.has_normals = hasattr(self, 'unit_normal')
|
|
|
|
self.points = np.zeros((0, 3))
|
|
|
|
self.rgbs = np.zeros((0, 3))
|
|
|
|
if self.has_normals:
|
|
|
|
self.unit_normals = np.zeros((0, 3))
|
|
|
|
self.generate_points()
|
|
|
|
if center:
|
|
|
|
self.center().shift(center)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
def show(self):
|
|
|
|
Image.fromarray(disp.paint_mobject(self)).show()
|
|
|
|
|
|
|
|
def save_image(self, name = None):
|
|
|
|
Image.fromarray(disp.paint_mobject(self)).save(
|
|
|
|
os.path.join(MOVIE_DIR, (name or str(self)) + ".png")
|
|
|
|
)
|
|
|
|
|
|
|
|
def add_points(self, points, rgbs = None, color = None):
|
|
|
|
"""
|
|
|
|
points must be a Nx3 numpy array, as must rgbs if it is not None
|
|
|
|
"""
|
|
|
|
points = np.array(points)
|
|
|
|
num_new_points = points.shape[0]
|
|
|
|
self.points = np.append(self.points, points)
|
|
|
|
self.points = self.points.reshape((self.points.size / 3, 3))
|
|
|
|
if rgbs is None:
|
|
|
|
color = Color(color) if color else self.color
|
|
|
|
rgbs = np.array([color.get_rgb()] * num_new_points)
|
|
|
|
else:
|
|
|
|
if rgbs.shape != points.shape:
|
|
|
|
raise Exception("points and rgbs must have same shape")
|
2015-08-01 11:34:33 -07:00
|
|
|
self.rgbs = np.append(self.rgbs, rgbs)
|
|
|
|
self.rgbs = self.rgbs.reshape((self.rgbs.size / 3, 3))
|
2015-06-10 22:00:35 -07:00
|
|
|
if self.has_normals:
|
|
|
|
self.unit_normals = np.append(
|
|
|
|
self.unit_normals,
|
|
|
|
np.array([self.unit_normal(point) for point in points])
|
|
|
|
).reshape(self.points.shape)
|
|
|
|
return self
|
|
|
|
|
2015-08-17 11:12:56 -07:00
|
|
|
def add(self, *mobjects):
|
|
|
|
for mobject in mobjects:
|
|
|
|
self.add_points(mobject.points, mobject.rgbs)
|
|
|
|
return self
|
|
|
|
|
|
|
|
def repeat(self, count):
|
|
|
|
#Can make transition animations nicer
|
|
|
|
points, rgbs = deepcopy(self.points), deepcopy(self.rgbs)
|
|
|
|
for x in range(count - 1):
|
|
|
|
self.add_points(points, rgbs)
|
|
|
|
return self
|
|
|
|
|
|
|
|
def rotate(self, angle, axis = OUT):
|
2015-06-10 22:00:35 -07:00
|
|
|
t_rotation_matrix = np.transpose(rotation_matrix(angle, axis))
|
|
|
|
self.points = np.dot(self.points, t_rotation_matrix)
|
|
|
|
if self.has_normals:
|
|
|
|
self.unit_normals = np.dot(self.unit_normals, t_rotation_matrix)
|
|
|
|
return self
|
|
|
|
|
2015-08-17 11:12:56 -07:00
|
|
|
def rotate_in_place(self, angle, axis = OUT):
|
2015-06-10 22:00:35 -07:00
|
|
|
center = self.get_center()
|
|
|
|
self.shift(-center)
|
|
|
|
self.rotate(angle, axis)
|
|
|
|
self.shift(center)
|
|
|
|
return self
|
|
|
|
|
|
|
|
def shift(self, vector):
|
2015-08-17 11:12:56 -07:00
|
|
|
self.points += vector
|
2015-06-10 22:00:35 -07:00
|
|
|
return self
|
|
|
|
|
2015-06-22 10:14:53 -07:00
|
|
|
def wag(self, wag_direction = RIGHT, wag_axis = DOWN,
|
|
|
|
wag_factor = 1.0):
|
2015-06-13 19:00:23 -07:00
|
|
|
alphas = np.dot(self.points, np.transpose(wag_axis))
|
|
|
|
alphas -= min(alphas)
|
|
|
|
alphas /= max(alphas)
|
2015-06-22 10:14:53 -07:00
|
|
|
alphas = alphas**wag_factor
|
2015-06-13 19:00:23 -07:00
|
|
|
self.points += np.dot(
|
|
|
|
alphas.reshape((len(alphas), 1)),
|
2015-08-17 11:12:56 -07:00
|
|
|
np.array(wag_direction).reshape((1, self.DIM))
|
2015-06-13 19:00:23 -07:00
|
|
|
)
|
|
|
|
return self
|
|
|
|
|
2015-06-10 22:00:35 -07:00
|
|
|
def center(self):
|
|
|
|
self.shift(-self.get_center())
|
|
|
|
return self
|
|
|
|
|
2015-08-01 11:34:33 -07:00
|
|
|
#Wrapper functions for better naming
|
2015-08-17 11:12:56 -07:00
|
|
|
def to_corner(self, corner = LEFT+DOWN, buff = EDGE_BUFFER):
|
2015-06-19 08:31:02 -07:00
|
|
|
return self.align_on_border(corner, buff)
|
|
|
|
|
2015-08-17 11:12:56 -07:00
|
|
|
def to_edge(self, edge = LEFT, buff = EDGE_BUFFER):
|
2015-06-19 08:31:02 -07:00
|
|
|
return self.align_on_border(edge, buff)
|
|
|
|
|
2015-08-17 11:12:56 -07:00
|
|
|
def align_on_border(self, direction, buff = 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.
|
|
|
|
"""
|
2015-09-18 14:22:59 -07:00
|
|
|
shift_val = np.zeros(3)
|
2015-06-19 08:31:02 -07:00
|
|
|
space_dim = (SPACE_WIDTH, SPACE_HEIGHT)
|
|
|
|
for i in [0, 1]:
|
|
|
|
if direction[i] == 0:
|
|
|
|
continue
|
|
|
|
elif direction[i] > 0:
|
|
|
|
shift_val[i] = space_dim[i]-buff-max(self.points[:,i])
|
|
|
|
else:
|
|
|
|
shift_val[i] = -space_dim[i]+buff-min(self.points[:,i])
|
|
|
|
self.shift(shift_val)
|
|
|
|
return self
|
|
|
|
|
2015-06-10 22:00:35 -07:00
|
|
|
def scale(self, scale_factor):
|
|
|
|
self.points *= scale_factor
|
|
|
|
return self
|
|
|
|
|
|
|
|
def scale_in_place(self, scale_factor):
|
|
|
|
center = self.get_center()
|
|
|
|
return self.center().scale(scale_factor).shift(center)
|
|
|
|
|
2015-08-07 18:10:00 -07:00
|
|
|
def stretch(self, factor, dim):
|
|
|
|
self.points[:,dim] *= factor
|
|
|
|
return self
|
|
|
|
|
2015-08-01 11:34:33 -07:00
|
|
|
def stretch_to_fit(self, length, dim):
|
|
|
|
center = self.get_center()
|
|
|
|
old_length = max(self.points[:,dim]) - min(self.points[:,dim])
|
|
|
|
self.center()
|
2015-08-07 18:10:00 -07:00
|
|
|
self.stretch(length/old_length, dim)
|
2015-08-01 11:34:33 -07:00
|
|
|
self.shift(center)
|
|
|
|
return self
|
|
|
|
|
|
|
|
def stretch_to_fit_width(self, width):
|
|
|
|
return self.stretch_to_fit(width, 0)
|
|
|
|
|
|
|
|
def stretch_to_fit_height(self, height):
|
|
|
|
return self.stretch_to_fit(height, 1)
|
|
|
|
|
2015-06-10 22:00:35 -07:00
|
|
|
def pose_at_angle(self):
|
|
|
|
self.rotate(np.pi / 7)
|
|
|
|
self.rotate(np.pi / 7, [1, 0, 0])
|
|
|
|
return self
|
|
|
|
|
2015-08-03 22:23:00 -07:00
|
|
|
def replace(self, mobject, stretch = False):
|
2015-08-07 18:10:00 -07:00
|
|
|
if mobject.get_num_points() == 0:
|
|
|
|
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.scale(mobject.get_width()/self.get_width())
|
|
|
|
self.center().shift(mobject.get_center())
|
2015-08-01 11:34:33 -07:00
|
|
|
return self
|
|
|
|
|
2015-06-10 22:00:35 -07:00
|
|
|
def apply_function(self, function):
|
|
|
|
self.points = np.apply_along_axis(function, 1, self.points)
|
|
|
|
return self
|
|
|
|
|
|
|
|
def apply_complex_function(self, function):
|
|
|
|
def point_map((x, y, z)):
|
|
|
|
result = function(complex(x, y))
|
|
|
|
return (result.real, result.imag, 0)
|
|
|
|
return self.apply_function(point_map)
|
|
|
|
|
2015-08-13 18:15:05 -07:00
|
|
|
def highlight(self, color = "yellow", condition = None):
|
2015-06-10 22:00:35 -07:00
|
|
|
"""
|
|
|
|
Condition is function which takes in one arguments, (x, y, z).
|
|
|
|
"""
|
2015-06-27 04:49:10 -07:00
|
|
|
rgb = Color(color).get_rgb()
|
|
|
|
if condition:
|
|
|
|
to_change = np.apply_along_axis(condition, 1, self.points)
|
|
|
|
self.rgbs[to_change, :] = rgb
|
|
|
|
else:
|
|
|
|
self.rgbs[:,:] = rgb
|
2015-06-10 22:00:35 -07:00
|
|
|
return self
|
|
|
|
|
2015-06-19 08:31:02 -07:00
|
|
|
def fade(self, brightness = 0.5):
|
|
|
|
self.rgbs *= brightness
|
2015-06-10 22:00:35 -07:00
|
|
|
return self
|
|
|
|
|
|
|
|
def filter_out(self, condition):
|
|
|
|
to_eliminate = ~np.apply_along_axis(condition, 1, self.points)
|
|
|
|
self.points = self.points[to_eliminate]
|
|
|
|
self.rgbs = self.rgbs[to_eliminate]
|
|
|
|
return self
|
|
|
|
|
2015-06-27 04:49:10 -07:00
|
|
|
def sort_points(self, function = lambda p : p[0]):
|
|
|
|
"""
|
|
|
|
function is any map from R^3 to R
|
|
|
|
"""
|
2015-08-17 11:12:56 -07:00
|
|
|
indices = range(self.get_num_points())
|
|
|
|
indices.sort(
|
|
|
|
lambda *pair : cmp(*map(function, self.points[pair, :]))
|
|
|
|
)
|
|
|
|
self.points = self.points[indices]
|
|
|
|
self.rgbs = self.rgbs[indices]
|
2015-08-01 11:34:33 -07:00
|
|
|
return self
|
2015-06-27 04:49:10 -07:00
|
|
|
|
2015-06-10 22:00:35 -07:00
|
|
|
### Getters ###
|
2015-06-13 19:00:23 -07:00
|
|
|
|
|
|
|
def get_num_points(self):
|
2015-08-07 18:10:00 -07:00
|
|
|
return len(self.points)
|
2015-06-13 19:00:23 -07:00
|
|
|
|
2015-06-10 22:00:35 -07:00
|
|
|
def get_center(self):
|
2015-08-07 18:10:00 -07:00
|
|
|
return (np.max(self.points, 0) + np.min(self.points, 0))/2.0
|
|
|
|
|
|
|
|
def get_center_of_mass(self):
|
2015-06-10 22:00:35 -07:00
|
|
|
return np.apply_along_axis(np.mean, 0, self.points)
|
|
|
|
|
2015-08-12 14:24:36 -07:00
|
|
|
def get_boundary_point(self, direction):
|
2015-08-07 18:10:00 -07:00
|
|
|
return self.points[np.argmax(np.dot(self.points, direction))]
|
|
|
|
|
2015-08-17 11:12:56 -07:00
|
|
|
def get_edge_center(self, direction):
|
|
|
|
dim = np.argmax(map(abs, direction))
|
|
|
|
max_or_min_func = np.max if direction[dim] > 0 else np.min
|
2015-08-07 18:10:00 -07:00
|
|
|
result = self.get_center()
|
|
|
|
result[dim] = max_or_min_func(self.points[:,dim])
|
|
|
|
return result
|
|
|
|
|
|
|
|
def get_top(self):
|
2015-08-17 11:12:56 -07:00
|
|
|
return self.get_edge_center(UP)
|
2015-08-07 18:10:00 -07:00
|
|
|
|
|
|
|
def get_bottom(self):
|
2015-08-17 11:12:56 -07:00
|
|
|
return self.get_edge_center(DOWN)
|
2015-08-07 18:10:00 -07:00
|
|
|
|
|
|
|
def get_right(self):
|
2015-08-17 11:12:56 -07:00
|
|
|
return self.get_edge_center(RIGHT)
|
2015-08-07 18:10:00 -07:00
|
|
|
|
|
|
|
def get_left(self):
|
2015-08-17 11:12:56 -07:00
|
|
|
return self.get_edge_center(LEFT)
|
2015-08-07 18:10:00 -07:00
|
|
|
|
2015-06-10 22:00:35 -07:00
|
|
|
def get_width(self):
|
|
|
|
return np.max(self.points[:, 0]) - np.min(self.points[:, 0])
|
|
|
|
|
|
|
|
def get_height(self):
|
|
|
|
return np.max(self.points[:, 1]) - np.min(self.points[:, 1])
|
|
|
|
|
2015-06-19 08:31:02 -07:00
|
|
|
def get_color(self):
|
|
|
|
color = Color()
|
|
|
|
color.set_rgb(self.rgbs[0, :])
|
|
|
|
return color
|
|
|
|
|
2015-06-10 22:00:35 -07:00
|
|
|
### Stuff subclasses should deal with ###
|
|
|
|
def should_buffer_points(self):
|
|
|
|
# potentially changed in subclasses
|
|
|
|
return GENERALLY_BUFF_POINTS
|
|
|
|
|
|
|
|
def generate_points(self):
|
|
|
|
#Typically implemented in subclass, unless purposefully left blank
|
|
|
|
pass
|
|
|
|
|
|
|
|
### Static Methods ###
|
|
|
|
def align_data(mobject1, mobject2):
|
|
|
|
count1, count2 = mobject1.get_num_points(), mobject2.get_num_points()
|
|
|
|
if count1 == 0:
|
|
|
|
mobject1.add_points([(0, 0, 0)])
|
|
|
|
if count2 == 0:
|
|
|
|
mobject2.add_points([(0, 0, 0)])
|
|
|
|
if count1 == count2:
|
|
|
|
return
|
|
|
|
for attr in ['points', 'rgbs']:
|
|
|
|
new_arrays = make_even(getattr(mobject1, attr), getattr(mobject2, attr))
|
|
|
|
for array, mobject in zip(new_arrays, [mobject1, mobject2]):
|
|
|
|
setattr(mobject, attr, np.array(array))
|
|
|
|
|
|
|
|
def interpolate(mobject1, mobject2, target_mobject, alpha):
|
|
|
|
"""
|
|
|
|
Turns target_mobject into an interpolation between mobject1
|
|
|
|
and mobject2.
|
|
|
|
"""
|
|
|
|
Mobject.align_data(mobject1, mobject2)
|
|
|
|
for attr in ['points', 'rgbs']:
|
|
|
|
new_array = (1 - alpha) * getattr(mobject1, attr) + \
|
|
|
|
alpha * getattr(mobject2, attr)
|
|
|
|
setattr(target_mobject, attr, new_array)
|
|
|
|
|
|
|
|
class Mobject1D(Mobject):
|
|
|
|
def __init__(self, density = DEFAULT_POINT_DENSITY_1D, *args, **kwargs):
|
|
|
|
self.epsilon = 1.0 / density
|
|
|
|
|
|
|
|
Mobject.__init__(self, *args, **kwargs)
|
|
|
|
|
|
|
|
class Mobject2D(Mobject):
|
|
|
|
def __init__(self, density = DEFAULT_POINT_DENSITY_2D, *args, **kwargs):
|
|
|
|
self.epsilon = 1.0 / density
|
|
|
|
Mobject.__init__(self, *args, **kwargs)
|
|
|
|
|
|
|
|
class CompoundMobject(Mobject):
|
|
|
|
def __init__(self, *mobjects):
|
|
|
|
Mobject.__init__(self)
|
|
|
|
self.original_mobs_num_points = []
|
|
|
|
for mobject in mobjects:
|
|
|
|
self.original_mobs_num_points.append(mobject.points.shape[0])
|
|
|
|
self.add_points(mobject.points, mobject.rgbs)
|
|
|
|
|
|
|
|
def split(self):
|
|
|
|
result = []
|
|
|
|
curr = 0
|
|
|
|
for num_points in self.original_mobs_num_points:
|
|
|
|
result.append(Mobject().add_points(
|
|
|
|
self.points[curr:curr+num_points, :],
|
|
|
|
self.rgbs[curr:curr+num_points, :]
|
|
|
|
))
|
|
|
|
curr += num_points
|
|
|
|
return result
|
|
|
|
|
2015-08-17 11:12:56 -07:00
|
|
|
# class CompoundMobject(Mobject):
|
|
|
|
# """
|
|
|
|
# Treats a collection of mobjects as if they were one.
|
|
|
|
|
|
|
|
# A weird form of inhertance is at play here...
|
|
|
|
# """
|
|
|
|
# def __init__(self, *mobjects):
|
|
|
|
# Mobject.__init__(self)
|
|
|
|
# self.mobjects = mobjects
|
|
|
|
# name_to_method = dict(
|
|
|
|
# inspect.getmembers(Mobject, predicate = inspect.ismethod)
|
|
|
|
# )
|
|
|
|
# names = name_to_method.keys()
|
|
|
|
# #Most reductions take the form of mapping a given method across
|
|
|
|
# #all constituent mobjects, then just returning self.
|
|
|
|
# name_to_reduce = dict([
|
|
|
|
# (name, lambda list : self)
|
|
|
|
# for name in names
|
|
|
|
# ])
|
|
|
|
# name_to_reduce.update(self.get_special_reduce_functions())
|
|
|
|
# def make_pseudo_method(name):
|
|
|
|
# return lambda *args, **kwargs : name_to_reduce[name]([
|
|
|
|
# name_to_method[name](mob, *args, **kwargs)
|
|
|
|
# for mob in self.mobjects
|
|
|
|
# ])
|
|
|
|
# for name in names:
|
|
|
|
# setattr(self, name, make_pseudo_method(name))
|
|
|
|
|
|
|
|
# def show(self):
|
|
|
|
|
|
|
|
|
|
|
|
# def get_special_reduce_functions(self):
|
|
|
|
# return {}
|
|
|
|
|
|
|
|
# def handle_method(self, method_name, *args, **kwargs):
|
|
|
|
# pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2015-06-10 22:00:35 -07:00
|
|
|
|