Better creatures and image mobject caching

This commit is contained in:
Grant Sanderson
2015-08-17 11:12:56 -07:00
parent 5fd08cddbb
commit 7087899f12
8 changed files with 308 additions and 262 deletions

View File

@ -51,8 +51,12 @@ GIF_DIR = os.path.join(FILE_DIR, "gifs")
MOVIE_DIR = os.path.join(FILE_DIR, "movies") MOVIE_DIR = os.path.join(FILE_DIR, "movies")
TEX_DIR = os.path.join(FILE_DIR, "Tex") TEX_DIR = os.path.join(FILE_DIR, "Tex")
TEX_IMAGE_DIR = os.path.join(IMAGE_DIR, "Tex") TEX_IMAGE_DIR = os.path.join(IMAGE_DIR, "Tex")
MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects")
IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image")
TMP_IMAGE_DIR = "/tmp/animation_images/" TMP_IMAGE_DIR = "/tmp/animation_images/"
for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TEX_DIR, TMP_IMAGE_DIR, TEX_IMAGE_DIR]:
for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TEX_DIR, TMP_IMAGE_DIR,
TEX_IMAGE_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR]:
if not os.path.exists(folder): if not os.path.exists(folder):
os.mkdir(folder) os.mkdir(folder)
@ -69,5 +73,25 @@ DARK_BLUE = "#236B8E"
DARK_BROWN = "#8B4513" DARK_BROWN = "#8B4513"
LIGHT_BROWN = "#CD853F" LIGHT_BROWN = "#CD853F"
PI_CREATURE_DIR = os.path.join(IMAGE_DIR, "PiCreature")
PI_CREATURE_PART_NAME_TO_DIR = lambda name : os.path.join(PI_CREATURE_DIR, "pi_creature_"+name) + ".png"
PI_CREATURE_SCALE_VAL = 0.5
PI_CREATURE_MOUTH_TO_EYES_DISTANCE = 0.25

View File

@ -9,6 +9,9 @@ import operator as op
from constants import * from constants import *
def interpolate(start, end, alpha):
return (1-alpha)*start + alpha*end
def center_of_mass(points): def center_of_mass(points):
points = [np.array(point).astype("float") for point in points] points = [np.array(point).astype("float") for point in points]
return sum(points) / len(points) return sum(points) / len(points)

View File

@ -7,108 +7,95 @@ from mobject import *
from simple_mobjects import * from simple_mobjects import *
class PiCreature(Mobject): class PiCreature(CompoundMobject):
DEFAULT_COLOR = "blue" DEFAULT_COLOR = "blue"
def __init__(self, **kwargs): PART_NAMES = [
Mobject.__init__(self, **kwargs) 'arm',
color = self.DEFAULT_COLOR 'body',
scale_val = 0.5 'left_eye',
mouth_to_eyes_distance = 0.25 'right_eye',
part_names = [ 'left_leg',
'arm', 'right_leg',
'body', 'mouth',
'left_eye', ]
'right_eye', WHITE_PART_NAMES = ['left_eye', 'right_eye', 'mouth']
'left_leg', MOUTH_NAMES = ["smile", "frown", "straight_mouth"]
'right_leg',
'mouth',
]
white_part_names = ['left_eye', 'right_eye', 'mouth']
directory = os.path.join(IMAGE_DIR, "PiCreature")
parts = [] def __init__(self, **kwargs):
self.white_parts = [] color = self.DEFAULT_COLOR if "color" not in kwargs else kwargs.pop("color")
for part_name in part_names: for part_name in self.PART_NAMES:
path = os.path.join(directory, "pi_creature_"+part_name) mob = ImageMobject(
path += ".png" PI_CREATURE_PART_NAME_TO_DIR(part_name)
mob = ImageMobject(path) )
mob.scale(scale_val) if part_name not in self.WHITE_PART_NAMES:
if part_name in white_part_names:
self.white_parts.append(mob)
else:
mob.highlight(color) mob.highlight(color)
mob.scale(PI_CREATURE_SCALE_VAL)
setattr(self, part_name, mob) setattr(self, part_name, mob)
parts.append(mob)
self.mouth.center().shift(
self.left_eye.get_center()/2 +
self.right_eye.get_center()/2 -
(0, mouth_to_eyes_distance, 0)
)
self.eyes = [self.left_eye, self.right_eye] self.eyes = [self.left_eye, self.right_eye]
self.legs = [self.left_leg, self.right_leg] self.legs = [self.left_leg, self.right_leg]
for part in parts: mouth_center = self.get_mouth_center()
self.add(part) self.mouth.center()
self.parts = parts self.smile = deepcopy(self.mouth)
self.frown = deepcopy(self.mouth).rotate(np.pi, RIGHT)
self.straight_mouth = tex_mobject("-").scale(0.5)
for mouth_name in ["mouth"] + self.MOUTH_NAMES:
mouth = getattr(self, mouth_name)
mouth.sort_points(lambda p : p[0])
mouth.shift(mouth_center)
#Ordering matters here, so hidden mouths are behind body
self.part_names = self.MOUTH_NAMES + self.PART_NAMES
self.white_parts = self.MOUTH_NAMES + self.WHITE_PART_NAMES
CompoundMobject.__init__(
self,
*self.get_parts(),
**kwargs
)
def rewire_part_attributes(self, self_from_parts = False): def sync_parts(self):
if self_from_parts: CompoundMobject.__init__(self, *self.get_parts())
total_num_points = sum(map(Mobject.get_num_points, self.parts))
self.points = np.zeros((total_num_points, Mobject.DIM))
self.rgbs = np.zeros((total_num_points, Mobject.DIM))
curr = 0
for part in self.parts:
n_points = part.get_num_points()
if self_from_parts:
self.points[curr:curr+n_points,:] = part.points
self.rgbs[curr:curr+n_points,:] = part.rgbs
else:
part.points = self.points[curr:curr+n_points,:]
part.rgbs = self.rgbs[curr:curr+n_points,:]
curr += n_points
return self return self
def reload_from_parts(self): def TODO_what_should_I_do_with_this(self):
self.rewire_part_attributes(self_from_parts = True) for part_name, mob in zip(self.part_names, self.split()):
setattr(self, part_name, mob)
def get_parts(self):
return [getattr(self, pn) for pn in self.part_names]
def get_white_parts(self):
return [getattr(self, pn) for pn in self.white_parts]
def get_mouth_center(self):
left_center = self.left_eye.get_center()
right_center = self.right_eye.get_center()
l_to_r = right_center-left_center
eyes_to_mouth = rotate_vector(l_to_r, -np.pi/2, OUT)
eyes_to_mouth /= np.linalg.norm(eyes_to_mouth)
return left_center/2 + right_center/2 + \
PI_CREATURE_MOUTH_TO_EYES_DISTANCE*eyes_to_mouth
def highlight(self, color, condition = None): def highlight(self, color, condition = None):
self.rewire_part_attributes() for part in set(self.get_parts()).difference(self.get_white_parts()):
if condition is not None: part.highlight(color, condition)
Mobject.highlight(self, color, condition) return self.sync_parts()
return self
for part in set(self.parts).difference(self.white_parts):
part.highlight(color)
self.reload_from_parts()
return self
def move_to(self, destination): def move_to(self, destination):
bottom = np.array(( self.shift(destination-self.get_bottom())
np.mean(self.points[:,0]), return self.sync_parts()
min(self.points[:,1]),
0 def change_mouth_to(self, mouth_name):
)) self.mouth = getattr(self, mouth_name)
self.shift(destination-bottom) return self.sync_parts()
self.rewire_part_attributes()
return self def give_smile(self):
return self.change_mouth_to("smile")
def give_frown(self): def give_frown(self):
center = self.mouth.get_center() return self.change_mouth_to("frown")
self.mouth.center()
self.mouth.apply_function(lambda (x, y, z) : (x, -y, z))
self.mouth.shift(center)
self.reload_from_parts()
return self
def give_straight_face(self): def give_straight_face(self):
center = self.mouth.get_center() return self.change_mouth_to("straight_mouth")
self.mouth.center()
new_mouth = tex_mobject("-").scale(0.5)
new_mouth.center().shift(self.mouth.get_center())
new_mouth.shift(center)
self.parts[self.parts.index(self.mouth)] = new_mouth
self.white_parts[self.white_parts.index(self.mouth)] = new_mouth
self.mouth = new_mouth
self.reload_from_parts()
return self
def get_eye_center(self): def get_eye_center(self):
return center_of_mass([ return center_of_mass([
@ -123,7 +110,7 @@ class PiCreature(Mobject):
for eye in self.left_eye, self.right_eye: for eye in self.left_eye, self.right_eye:
eye.highlight("black", should_delete) eye.highlight("black", should_delete)
self.give_straight_face() self.give_straight_face()
return self return self.sync_parts()
def make_sad(self): def make_sad(self):
eye_x, eye_y = self.get_eye_center()[:2] eye_x, eye_y = self.get_eye_center()[:2]
@ -133,13 +120,11 @@ class PiCreature(Mobject):
for eye in self.left_eye, self.right_eye: for eye in self.left_eye, self.right_eye:
eye.highlight("black", should_delete) eye.highlight("black", should_delete)
self.give_frown() self.give_frown()
self.reload_from_parts() return self.sync_parts()
return self
def get_step_intermediate(self, pi_creature): def get_step_intermediate(self, pi_creature):
vect = pi_creature.get_center() - self.get_center() vect = pi_creature.get_center() - self.get_center()
result = deepcopy(self).shift(vect / 2.0) result = deepcopy(self).shift(vect / 2.0)
result.rewire_part_attributes()
left_forward = vect[0] > 0 left_forward = vect[0] > 0
if self.right_leg.get_center()[0] < self.left_leg.get_center()[0]: if self.right_leg.get_center()[0] < self.left_leg.get_center()[0]:
#For Mortimer's case #For Mortimer's case
@ -150,31 +135,26 @@ class PiCreature(Mobject):
else: else:
result.right_leg.wag(vect/2.0, DOWN) result.right_leg.wag(vect/2.0, DOWN)
result.left_leg.wag(-vect/2.0, DOWN) result.left_leg.wag(-vect/2.0, DOWN)
return result return result.sync_parts()
def blink(self): def blink(self):
for eye in self.left_eye, self.right_eye: for eye in self.left_eye, self.right_eye:
bottom = min(eye.points[:,1]) bottom = eye.get_bottom()
eye.apply_function( eye.apply_function(
lambda (x, y, z) : (x, bottom, z) lambda (x, y, z) : (x, bottom[1], z)
) )
self.reload_from_parts() return self.sync_parts()
return self
def shift_eyes(self): def shift_eyes(self):
for eye in self.left_eye, self.right_eye: for eye in self.left_eye, self.right_eye:
center = eye.get_center() eye.rotate_in_place(np.pi, UP)
eye.center() return self.sync_parts()
eye.rotate(np.pi, UP)
eye.shift(center)
self.reload_from_parts()
return self
def to_symbol(self): def to_symbol(self):
for white_part in self.white_parts: CompoundMobject.__init__(
self.parts.remove(white_part) self,
self.reload_from_parts() *list(set(self.get_parts()).difference(self.get_white_parts()))
return self )
class Randolph(PiCreature): class Randolph(PiCreature):
@ -187,18 +167,6 @@ class Mortimer(PiCreature):
# self.highlight(DARK_BROWN) # self.highlight(DARK_BROWN)
self.give_straight_face() self.give_straight_face()
self.rotate(np.pi, UP) self.rotate(np.pi, UP)
self.rewire_part_attributes()
class TauCreature(PiCreature):
def __init__(self, **kwargs):
leg_shift_val = 0.25
leg_wag_val = 0.2
PiCreature.__init__(self, **kwargs)
self.parts.remove(self.right_leg)
self.left_leg.shift(leg_shift_val*RIGHT)
self.left_leg.wag(leg_wag_val*RIGHT, DOWN)
self.leg = self.left_leg
self.reload_from_parts()

View File

@ -13,42 +13,56 @@ class ImageMobject(Mobject2D):
""" """
# SHOULD_BUFF_POINTS = False # SHOULD_BUFF_POINTS = False
def __init__(self, def __init__(self,
image, image_file,
filter_color = "black", filter_color = "black",
invert = True, invert = True,
*args, **kwargs): *args, **kwargs):
#TODO, Make sure you always convert to RGB
self.filter_rgb = 255 * np.array(Color(filter_color).get_rgb()).astype('uint8')
if isinstance(image, str):
self.name = to_cammel_case(
os.path.split(image)[-1].split(".")[0]
)
possible_paths = [
image,
os.path.join(IMAGE_DIR, image),
os.path.join(IMAGE_DIR, image + ".jpg"),
os.path.join(IMAGE_DIR, image + ".png"),
]
found = False
for path in possible_paths:
if os.path.exists(path):
image = Image.open(path).convert('RGB')
found = True
if not found:
raise IOError("File not Found")
if invert:
image = invert_image(image)
self.image_array = np.array(image)
Mobject2D.__init__(self, *args, **kwargs) Mobject2D.__init__(self, *args, **kwargs)
self.filter_rgb = 255 * np.array(Color(filter_color).get_rgb()).astype('uint8')
self.name = to_cammel_case(
os.path.split(image_file)[-1].split(".")[0]
)
possible_paths = [
image_file,
os.path.join(IMAGE_DIR, image_file),
os.path.join(IMAGE_DIR, image_file + ".jpg"),
os.path.join(IMAGE_DIR, image_file + ".png"),
]
for path in possible_paths:
if os.path.exists(path):
self.generate_points_from_file(path, invert)
return
raise IOError("File not Found")
def generate_points(self): def generate_points_from_file(self, path, invert):
height, width = self.image_array.shape[:2] #Hash should be unique to (path, invert) pair
dtype = 'float'
unique_hash = str(hash(path+str(invert)))
cached_points, cached_rgbs = [
os.path.join(IMAGE_MOBJECT_DIR, unique_hash)+extension
for extension in ".points", ".rgbs"
]
if os.path.exists(cached_points) and os.path.exists(cached_rgbs):
self.points = np.fromfile(cached_points, dtype = dtype)
self.rgbs = np.fromfile(cached_rgbs, dtype = dtype)
n_points = self.points.size/self.DIM
self.points = self.points.reshape(n_points, self.DIM)
self.rgbs = self.rgbs.reshape(n_points, 3)
else:
image = Image.open(path).convert('RGB')
if invert:
image = invert_image(image)
self.generate_points_from_image_array(np.array(image))
self.points.astype(dtype).tofile(cached_points)
self.rgbs.astype(dtype).tofile(cached_rgbs)
def generate_points_from_image_array(self, image_array):
height, width = image_array.shape[:2]
#Flatten array, and find indices where rgb is not filter_rgb #Flatten array, and find indices where rgb is not filter_rgb
array = self.image_array.reshape((height * width, 3)) array = image_array.reshape((height * width, 3))
ones = np.ones(height * width, dtype = 'bool') bools = array == self.filter_rgb
for i in range(3): bools = bools[:,0]*bools[:,1]*bools[:,2]
ones *= (array[:,i] != self.filter_rgb[i]) indices = np.arange(height * width, dtype = 'int')[~bools]
indices = np.arange(height * width, dtype = 'int')[ones]
rgbs = array[indices, :].astype('float') / 255.0 rgbs = array[indices, :].astype('float') / 255.0
points = np.zeros((indices.size, 3), dtype = 'float64') points = np.zeros((indices.size, 3), dtype = 'float64')
@ -94,12 +108,12 @@ def tex_mobject(expression,
else: else:
size = "\\large" size = "\\large"
#Todo, make this more sophisticated. #Todo, make this more sophisticated.
images = tex_to_image(expression, size, template_tex_file) image_files = tex_to_image(expression, size, template_tex_file)
if isinstance(images, list): if isinstance(image_files, list):
#TODO, is checking listiness really the best here? #TODO, is checking listiness really the best here?
result = CompoundMobject(*map(ImageMobject, images)) result = CompoundMobject(*map(ImageMobject, image_files))
else: else:
result = ImageMobject(images) result = ImageMobject(image_files)
return result.highlight("white").center() return result.highlight("white").center()

View File

@ -5,6 +5,7 @@ from PIL import Image
from random import random from random import random
from copy import deepcopy from copy import deepcopy
from colour import Color from colour import Color
import inspect
from constants import * from constants import *
from helpers import * from helpers import *
@ -17,10 +18,9 @@ class Mobject(object):
""" """
#Number of numbers used to describe a point (3 for pos, 3 for normal vector) #Number of numbers used to describe a point (3 for pos, 3 for normal vector)
DIM = 3 DIM = 3
DEFAULT_COLOR = Color("skyblue") DEFAULT_COLOR = Color("skyblue")
SHOULD_BUFF_POINTS = GENERALLY_BUFF_POINTS SHOULD_BUFF_POINTS = GENERALLY_BUFF_POINTS
EDGE_BUFFER = 0.5
def __init__(self, def __init__(self,
color = None, color = None,
@ -74,14 +74,26 @@ class Mobject(object):
).reshape(self.points.shape) ).reshape(self.points.shape)
return self return self
def rotate(self, angle, axis = [0, 0, 1]): 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):
t_rotation_matrix = np.transpose(rotation_matrix(angle, axis)) t_rotation_matrix = np.transpose(rotation_matrix(angle, axis))
self.points = np.dot(self.points, t_rotation_matrix) self.points = np.dot(self.points, t_rotation_matrix)
if self.has_normals: if self.has_normals:
self.unit_normals = np.dot(self.unit_normals, t_rotation_matrix) self.unit_normals = np.dot(self.unit_normals, t_rotation_matrix)
return self return self
def rotate_in_place(self, angle, axis = (0, 0, 1)): def rotate_in_place(self, angle, axis = OUT):
center = self.get_center() center = self.get_center()
self.shift(-center) self.shift(-center)
self.rotate(angle, axis) self.rotate(angle, axis)
@ -89,12 +101,7 @@ class Mobject(object):
return self return self
def shift(self, vector): def shift(self, vector):
cycle = it.cycle(vector) self.points += vector
v = np.array([
cycle.next()
for x in range(self.points.size)
]).reshape(self.points.shape)
self.points += v
return self return self
def wag(self, wag_direction = RIGHT, wag_axis = DOWN, def wag(self, wag_direction = RIGHT, wag_axis = DOWN,
@ -105,7 +112,7 @@ class Mobject(object):
alphas = alphas**wag_factor alphas = alphas**wag_factor
self.points += np.dot( self.points += np.dot(
alphas.reshape((len(alphas), 1)), alphas.reshape((len(alphas), 1)),
np.array(wag_direction).reshape((1, 3)) np.array(wag_direction).reshape((1, self.DIM))
) )
return self return self
@ -114,18 +121,18 @@ class Mobject(object):
return self return self
#Wrapper functions for better naming #Wrapper functions for better naming
def to_corner(self, corner = (-1, 1, 0), buff = 0.5): def to_corner(self, corner = LEFT+DOWN, buff = EDGE_BUFFER):
return self.align_on_border(corner, buff) return self.align_on_border(corner, buff)
def to_edge(self, edge = (-1, 0, 0), buff = 0.5): def to_edge(self, edge = LEFT, buff = EDGE_BUFFER):
return self.align_on_border(edge, buff) return self.align_on_border(edge, buff)
def align_on_border(self, direction, buff = 0.5): def align_on_border(self, direction, buff = EDGE_BUFFER):
""" """
Direction just needs to be a vector pointing towards side or Direction just needs to be a vector pointing towards side or
corner in the 2d plane. corner in the 2d plane.
""" """
shift_val = [0, 0, 0] shift_val = ORIGIN
space_dim = (SPACE_WIDTH, SPACE_HEIGHT) space_dim = (SPACE_WIDTH, SPACE_HEIGHT)
for i in [0, 1]: for i in [0, 1]:
if direction[i] == 0: if direction[i] == 0:
@ -137,7 +144,6 @@ class Mobject(object):
self.shift(shift_val) self.shift(shift_val)
return self return self
def scale(self, scale_factor): def scale(self, scale_factor):
self.points *= scale_factor self.points *= scale_factor
return self return self
@ -164,18 +170,6 @@ class Mobject(object):
def stretch_to_fit_height(self, height): def stretch_to_fit_height(self, height):
return self.stretch_to_fit(height, 1) return self.stretch_to_fit(height, 1)
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 pose_at_angle(self): def pose_at_angle(self):
self.rotate(np.pi / 7) self.rotate(np.pi / 7)
self.rotate(np.pi / 7, [1, 0, 0]) self.rotate(np.pi / 7, [1, 0, 0])
@ -229,10 +223,12 @@ class Mobject(object):
""" """
function is any map from R^3 to R function is any map from R^3 to R
""" """
self.points = np.array(sorted( indices = range(self.get_num_points())
self.points, indices.sort(
lambda *points : cmp(*map(function, points)) lambda *pair : cmp(*map(function, self.points[pair, :]))
)) )
self.points = self.points[indices]
self.rgbs = self.rgbs[indices]
return self return self
### Getters ### ### Getters ###
@ -249,22 +245,24 @@ class Mobject(object):
def get_boundary_point(self, direction): def get_boundary_point(self, direction):
return self.points[np.argmax(np.dot(self.points, direction))] return self.points[np.argmax(np.dot(self.points, direction))]
def get_edge_center(self, dim, max_or_min_func): 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
result = self.get_center() result = self.get_center()
result[dim] = max_or_min_func(self.points[:,dim]) result[dim] = max_or_min_func(self.points[:,dim])
return result return result
def get_top(self): def get_top(self):
return self.get_edge_center(1, np.max) return self.get_edge_center(UP)
def get_bottom(self): def get_bottom(self):
return self.get_edge_center(1, np.min) return self.get_edge_center(DOWN)
def get_right(self): def get_right(self):
return self.get_edge_center(0, np.max) return self.get_edge_center(RIGHT)
def get_left(self): def get_left(self):
return self.get_edge_center(0, np.min) return self.get_edge_center(LEFT)
def get_width(self): def get_width(self):
return np.max(self.points[:, 0]) - np.min(self.points[:, 0]) return np.max(self.points[:, 0]) - np.min(self.points[:, 0])
@ -341,4 +339,60 @@ class CompoundMobject(Mobject):
curr += num_points curr += num_points
return result return result
# 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

View File

@ -6,66 +6,22 @@ from image_mobject import text_mobject
from constants import * from constants import *
from helpers import * from helpers import *
class Point(Mobject): class Point(Mobject):
DEFAULT_COLOR = "black" DEFAULT_COLOR = "black"
def __init__(self, point = (0, 0, 0), *args, **kwargs): def __init__(self, location = ORIGIN, *args, **kwargs):
self.location = np.array(location)
Mobject.__init__(self, *args, **kwargs) Mobject.__init__(self, *args, **kwargs)
self.points = np.array(point).reshape(1, 3)
self.rgbs = np.array(self.color.get_rgb()).reshape(1, 3)
class Arrow(Mobject1D):
DEFAULT_COLOR = "white"
DEFAULT_NUDGE_DISTANCE = 0.1
def __init__(self,
point = (0, 0, 0),
direction = (-1, 1, 0),
tail = None,
length = 1,
tip_length = 0.25,
normal = (0, 0, 1),
density = DEFAULT_POINT_DENSITY_1D,
*args, **kwargs):
self.point = np.array(point)
if tail is not None:
direction = self.point - tail
length = np.linalg.norm(direction)
self.direction = np.array(direction) / np.linalg.norm(direction)
density *= max(length, 0.1)
self.length = length
self.normal = np.array(normal)
self.tip_length = tip_length
Mobject1D.__init__(self, density = density, **kwargs)
def generate_points(self): def generate_points(self):
self.add_points([ self.add_points(self.location.reshape((1, 3)))
[x, x, x] * self.direction + self.point
for x in np.arange(-self.length, 0, self.epsilon)
])
tips_dir = [np.array(-self.direction), np.array(-self.direction)]
for i, sgn in zip([0, 1], [-1, 1]):
tips_dir[i] = rotate_vector(tips_dir[i], sgn * np.pi / 4, self.normal)
self.add_points([
[x, x, x] * tips_dir[i] + self.point
for x in np.arange(0, self.tip_length, self.epsilon)
for i in [0, 1]
])
def nudge(self, distance = None):
if distance is None:
distance = self.DEFAULT_NUDGE_DISTANCE
return self.shift(-self.direction * distance)
class Vector(Arrow):
def __init__(self, point = (1, 0, 0), *args, **kwargs):
length = np.linalg.norm(point)
Arrow.__init__(self, point = point, direction = point,
length = length, tip_length = 0.2 * length,
*args, **kwargs)
class Dot(Mobject1D): #Use 1D density, even though 2D class Dot(Mobject1D): #Use 1D density, even though 2D
DEFAULT_COLOR = "white" DEFAULT_COLOR = "white"
DEFAULT_RADIUS = 0.05 DEFAULT_RADIUS = 0.05
def __init__(self, center = (0, 0, 0), radius = DEFAULT_RADIUS, *args, **kwargs): def __init__(self, center = ORIGIN, radius = DEFAULT_RADIUS,
*args, **kwargs):
center = np.array(center) center = np.array(center)
if center.size == 1: if center.size == 1:
raise Exception("Center must have 2 or 3 coordinates!") raise Exception("Center must have 2 or 3 coordinates!")
@ -94,15 +50,32 @@ class Cross(Mobject1D):
]) ])
class Line(Mobject1D): class Line(Mobject1D):
def __init__(self, start, end, density = DEFAULT_POINT_DENSITY_1D, *args, **kwargs): MIN_DENSITY = 0.1
self.start = np.array(start) def __init__(self, start, end, density = DEFAULT_POINT_DENSITY_1D,
self.end = np.array(end) *args, **kwargs):
density *= max(self.get_length(), 0.1) self.set_start_and_end(start, end)
density *= max(self.get_length(), self.MIN_DENSITY)
Mobject1D.__init__(self, density = density, *args, **kwargs) Mobject1D.__init__(self, density = density, *args, **kwargs)
def set_start_and_end(self, start, end):
preliminary_start, preliminary_end = [
arg.get_center()
if isinstance(arg, Mobject)
else np.array(arg)
for arg in start, end
]
start_to_end = preliminary_end - preliminary_start
longer_dim = np.argmax(map(abs, start_to_end))
self.start, self.end = [
arg.get_edge_center(unit*start_to_end)
if isinstance(arg, Mobject)
else np.array(arg)
for arg, unit in zip([start, end], [1, -1])
]
def generate_points(self): def generate_points(self):
self.add_points([ self.add_points([
t * self.end + (1 - t) * self.start interpolate(self.start, self.end, t)
for t in np.arange(0, 1, self.epsilon) for t in np.arange(0, 1, self.epsilon)
]) ])
@ -116,7 +89,7 @@ class Line(Mobject1D):
] ]
return rise/run return rise/run
class NewArrow(Line): class Arrow(Line):
DEFAULT_COLOR = "white" DEFAULT_COLOR = "white"
DEFAULT_TIP_LENGTH = 0.25 DEFAULT_TIP_LENGTH = 0.25
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -128,22 +101,38 @@ class NewArrow(Line):
self.add_tip(tip_length) self.add_tip(tip_length)
def add_tip(self, tip_length): def add_tip(self, tip_length):
pass vect = self.start-self.end
vect *= tip_length/np.linalg.norm(vect)
self.add_points([
interpolate(self.end, self.end+v, t)
for t in np.arange(0, 1, tip_length*self.epsilon)
for v in [
rotate_vector(vect, np.pi/4, axis)
for axis in IN, OUT
]
])
class CurvedLine(Line): class CurvedLine(Line):
def __init__(self, start, end, via = None, *args, **kwargs): def __init__(self, start, end, via = None, *args, **kwargs):
self.set_start_and_end(start, end)
if via == None: if via == None:
via = rotate_vector( self.via = rotate_vector(
end - start, self.end - self.start,
np.pi/3, [0,0,1] np.pi/3, [0,0,1]
) + start ) + self.start
self.via = via elif isinstance(via, Mobject):
self.via = via.get_center()
else:
self.via = via
Line.__init__(self, start, end, *args, **kwargs) Line.__init__(self, start, end, *args, **kwargs)
def generate_points(self): def generate_points(self):
self.add_points([ self.add_points([
4*(0.25-t*(1-t))*(t*self.end + (1-t)*self.start) + interpolate(
4*t*(1-t)*self.via interpolate(self.start, self.end, t),
self.via,
t*(1-t)
)
for t in np.arange(0, 1, self.epsilon) for t in np.arange(0, 1, self.epsilon)
]) ])

View File

@ -10,20 +10,14 @@ from animation import *
from mobject import * from mobject import *
from constants import * from constants import *
from region import * from region import *
from scene import Scene, RearrangeEquation from scene import Scene
from script_wrapper import command_line_create_scene from script_wrapper import command_line_create_scene
class SampleScene(RearrangeEquation): class SampleScene(Scene):
def construct(self): def construct(self):
three = tex_mobject("3") three = tex_mobject("3")
three.sort_points(np.linalg.norm) self.add(three)
self.animate(DelayByOrder(ApplyMethod(three.scale, 3)))
self.dither()
if __name__ == "__main__": if __name__ == "__main__":
command_line_create_scene() command_line_create_scene()

View File

@ -19,7 +19,7 @@ def tex_to_image(expression,
image_dir = os.path.join(TEX_IMAGE_DIR, exp_hash) image_dir = os.path.join(TEX_IMAGE_DIR, exp_hash)
if os.path.exists(image_dir): if os.path.exists(image_dir):
result = [ result = [
Image.open(os.path.join(image_dir, png_file)).convert('RGB') os.path.join(image_dir, png_file)
for png_file in sorted( for png_file in sorted(
os.listdir(image_dir), os.listdir(image_dir),
cmp_enumerated_files cmp_enumerated_files
@ -92,7 +92,7 @@ def dvi_to_png(filename, regen_if_exists = False):
if name.endswith(".png") if name.endswith(".png")
] ]
image_paths.sort(cmp_enumerated_files) image_paths.sort(cmp_enumerated_files)
return [Image.open(path).convert('RGB') for path in image_paths] return image_paths
raise IOError("File not Found") raise IOError("File not Found")
def cmp_enumerated_files(name1, name2): def cmp_enumerated_files(name1, name2):