mirror of
https://github.com/3b1b/manim.git
synced 2025-07-30 21:44:19 +08:00
Better creatures and image mobject caching
This commit is contained in:
26
constants.py
26
constants.py
@ -51,8 +51,12 @@ GIF_DIR = os.path.join(FILE_DIR, "gifs")
|
||||
MOVIE_DIR = os.path.join(FILE_DIR, "movies")
|
||||
TEX_DIR = os.path.join(FILE_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/"
|
||||
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):
|
||||
os.mkdir(folder)
|
||||
|
||||
@ -69,5 +73,25 @@ DARK_BLUE = "#236B8E"
|
||||
DARK_BROWN = "#8B4513"
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -9,6 +9,9 @@ import operator as op
|
||||
|
||||
from constants import *
|
||||
|
||||
def interpolate(start, end, alpha):
|
||||
return (1-alpha)*start + alpha*end
|
||||
|
||||
def center_of_mass(points):
|
||||
points = [np.array(point).astype("float") for point in points]
|
||||
return sum(points) / len(points)
|
||||
|
@ -7,14 +7,9 @@ from mobject import *
|
||||
from simple_mobjects import *
|
||||
|
||||
|
||||
class PiCreature(Mobject):
|
||||
class PiCreature(CompoundMobject):
|
||||
DEFAULT_COLOR = "blue"
|
||||
def __init__(self, **kwargs):
|
||||
Mobject.__init__(self, **kwargs)
|
||||
color = self.DEFAULT_COLOR
|
||||
scale_val = 0.5
|
||||
mouth_to_eyes_distance = 0.25
|
||||
part_names = [
|
||||
PART_NAMES = [
|
||||
'arm',
|
||||
'body',
|
||||
'left_eye',
|
||||
@ -23,92 +18,84 @@ class PiCreature(Mobject):
|
||||
'right_leg',
|
||||
'mouth',
|
||||
]
|
||||
white_part_names = ['left_eye', 'right_eye', 'mouth']
|
||||
directory = os.path.join(IMAGE_DIR, "PiCreature")
|
||||
WHITE_PART_NAMES = ['left_eye', 'right_eye', 'mouth']
|
||||
MOUTH_NAMES = ["smile", "frown", "straight_mouth"]
|
||||
|
||||
parts = []
|
||||
self.white_parts = []
|
||||
for part_name in part_names:
|
||||
path = os.path.join(directory, "pi_creature_"+part_name)
|
||||
path += ".png"
|
||||
mob = ImageMobject(path)
|
||||
mob.scale(scale_val)
|
||||
if part_name in white_part_names:
|
||||
self.white_parts.append(mob)
|
||||
else:
|
||||
mob.highlight(color)
|
||||
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)
|
||||
def __init__(self, **kwargs):
|
||||
color = self.DEFAULT_COLOR if "color" not in kwargs else kwargs.pop("color")
|
||||
for part_name in self.PART_NAMES:
|
||||
mob = ImageMobject(
|
||||
PI_CREATURE_PART_NAME_TO_DIR(part_name)
|
||||
)
|
||||
if part_name not in self.WHITE_PART_NAMES:
|
||||
mob.highlight(color)
|
||||
mob.scale(PI_CREATURE_SCALE_VAL)
|
||||
setattr(self, part_name, mob)
|
||||
self.eyes = [self.left_eye, self.right_eye]
|
||||
self.legs = [self.left_leg, self.right_leg]
|
||||
for part in parts:
|
||||
self.add(part)
|
||||
self.parts = parts
|
||||
mouth_center = self.get_mouth_center()
|
||||
self.mouth.center()
|
||||
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):
|
||||
if self_from_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
|
||||
def sync_parts(self):
|
||||
CompoundMobject.__init__(self, *self.get_parts())
|
||||
return self
|
||||
|
||||
def reload_from_parts(self):
|
||||
self.rewire_part_attributes(self_from_parts = True)
|
||||
def TODO_what_should_I_do_with_this(self):
|
||||
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):
|
||||
self.rewire_part_attributes()
|
||||
if condition is not None:
|
||||
Mobject.highlight(self, color, condition)
|
||||
return self
|
||||
for part in set(self.parts).difference(self.white_parts):
|
||||
part.highlight(color)
|
||||
self.reload_from_parts()
|
||||
return self
|
||||
for part in set(self.get_parts()).difference(self.get_white_parts()):
|
||||
part.highlight(color, condition)
|
||||
return self.sync_parts()
|
||||
|
||||
def move_to(self, destination):
|
||||
bottom = np.array((
|
||||
np.mean(self.points[:,0]),
|
||||
min(self.points[:,1]),
|
||||
0
|
||||
))
|
||||
self.shift(destination-bottom)
|
||||
self.rewire_part_attributes()
|
||||
return self
|
||||
self.shift(destination-self.get_bottom())
|
||||
return self.sync_parts()
|
||||
|
||||
def change_mouth_to(self, mouth_name):
|
||||
self.mouth = getattr(self, mouth_name)
|
||||
return self.sync_parts()
|
||||
|
||||
def give_smile(self):
|
||||
return self.change_mouth_to("smile")
|
||||
|
||||
def give_frown(self):
|
||||
center = self.mouth.get_center()
|
||||
self.mouth.center()
|
||||
self.mouth.apply_function(lambda (x, y, z) : (x, -y, z))
|
||||
self.mouth.shift(center)
|
||||
self.reload_from_parts()
|
||||
return self
|
||||
return self.change_mouth_to("frown")
|
||||
|
||||
def give_straight_face(self):
|
||||
center = self.mouth.get_center()
|
||||
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
|
||||
return self.change_mouth_to("straight_mouth")
|
||||
|
||||
def get_eye_center(self):
|
||||
return center_of_mass([
|
||||
@ -123,7 +110,7 @@ class PiCreature(Mobject):
|
||||
for eye in self.left_eye, self.right_eye:
|
||||
eye.highlight("black", should_delete)
|
||||
self.give_straight_face()
|
||||
return self
|
||||
return self.sync_parts()
|
||||
|
||||
def make_sad(self):
|
||||
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:
|
||||
eye.highlight("black", should_delete)
|
||||
self.give_frown()
|
||||
self.reload_from_parts()
|
||||
return self
|
||||
return self.sync_parts()
|
||||
|
||||
def get_step_intermediate(self, pi_creature):
|
||||
vect = pi_creature.get_center() - self.get_center()
|
||||
result = deepcopy(self).shift(vect / 2.0)
|
||||
result.rewire_part_attributes()
|
||||
left_forward = vect[0] > 0
|
||||
if self.right_leg.get_center()[0] < self.left_leg.get_center()[0]:
|
||||
#For Mortimer's case
|
||||
@ -150,31 +135,26 @@ class PiCreature(Mobject):
|
||||
else:
|
||||
result.right_leg.wag(vect/2.0, DOWN)
|
||||
result.left_leg.wag(-vect/2.0, DOWN)
|
||||
return result
|
||||
return result.sync_parts()
|
||||
|
||||
def blink(self):
|
||||
for eye in self.left_eye, self.right_eye:
|
||||
bottom = min(eye.points[:,1])
|
||||
bottom = eye.get_bottom()
|
||||
eye.apply_function(
|
||||
lambda (x, y, z) : (x, bottom, z)
|
||||
lambda (x, y, z) : (x, bottom[1], z)
|
||||
)
|
||||
self.reload_from_parts()
|
||||
return self
|
||||
return self.sync_parts()
|
||||
|
||||
def shift_eyes(self):
|
||||
for eye in self.left_eye, self.right_eye:
|
||||
center = eye.get_center()
|
||||
eye.center()
|
||||
eye.rotate(np.pi, UP)
|
||||
eye.shift(center)
|
||||
self.reload_from_parts()
|
||||
return self
|
||||
eye.rotate_in_place(np.pi, UP)
|
||||
return self.sync_parts()
|
||||
|
||||
def to_symbol(self):
|
||||
for white_part in self.white_parts:
|
||||
self.parts.remove(white_part)
|
||||
self.reload_from_parts()
|
||||
return self
|
||||
CompoundMobject.__init__(
|
||||
self,
|
||||
*list(set(self.get_parts()).difference(self.get_white_parts()))
|
||||
)
|
||||
|
||||
|
||||
class Randolph(PiCreature):
|
||||
@ -187,18 +167,6 @@ class Mortimer(PiCreature):
|
||||
# self.highlight(DARK_BROWN)
|
||||
self.give_straight_face()
|
||||
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()
|
||||
|
||||
|
||||
|
||||
|
@ -13,42 +13,56 @@ class ImageMobject(Mobject2D):
|
||||
"""
|
||||
# SHOULD_BUFF_POINTS = False
|
||||
def __init__(self,
|
||||
image,
|
||||
image_file,
|
||||
filter_color = "black",
|
||||
invert = True,
|
||||
*args, **kwargs):
|
||||
#TODO, Make sure you always convert to RGB
|
||||
Mobject2D.__init__(self, *args, **kwargs)
|
||||
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]
|
||||
os.path.split(image_file)[-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"),
|
||||
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"),
|
||||
]
|
||||
found = False
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
image = Image.open(path).convert('RGB')
|
||||
found = True
|
||||
if not found:
|
||||
self.generate_points_from_file(path, invert)
|
||||
return
|
||||
raise IOError("File not Found")
|
||||
|
||||
def generate_points_from_file(self, path, invert):
|
||||
#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.image_array = np.array(image)
|
||||
Mobject2D.__init__(self, *args, **kwargs)
|
||||
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(self):
|
||||
height, width = self.image_array.shape[:2]
|
||||
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
|
||||
array = self.image_array.reshape((height * width, 3))
|
||||
ones = np.ones(height * width, dtype = 'bool')
|
||||
for i in range(3):
|
||||
ones *= (array[:,i] != self.filter_rgb[i])
|
||||
indices = np.arange(height * width, dtype = 'int')[ones]
|
||||
array = image_array.reshape((height * width, 3))
|
||||
bools = array == self.filter_rgb
|
||||
bools = bools[:,0]*bools[:,1]*bools[:,2]
|
||||
indices = np.arange(height * width, dtype = 'int')[~bools]
|
||||
rgbs = array[indices, :].astype('float') / 255.0
|
||||
|
||||
points = np.zeros((indices.size, 3), dtype = 'float64')
|
||||
@ -94,12 +108,12 @@ def tex_mobject(expression,
|
||||
else:
|
||||
size = "\\large"
|
||||
#Todo, make this more sophisticated.
|
||||
images = tex_to_image(expression, size, template_tex_file)
|
||||
if isinstance(images, list):
|
||||
image_files = tex_to_image(expression, size, template_tex_file)
|
||||
if isinstance(image_files, list):
|
||||
#TODO, is checking listiness really the best here?
|
||||
result = CompoundMobject(*map(ImageMobject, images))
|
||||
result = CompoundMobject(*map(ImageMobject, image_files))
|
||||
else:
|
||||
result = ImageMobject(images)
|
||||
result = ImageMobject(image_files)
|
||||
return result.highlight("white").center()
|
||||
|
||||
|
||||
|
@ -5,6 +5,7 @@ from PIL import Image
|
||||
from random import random
|
||||
from copy import deepcopy
|
||||
from colour import Color
|
||||
import inspect
|
||||
|
||||
from constants 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)
|
||||
DIM = 3
|
||||
|
||||
DEFAULT_COLOR = Color("skyblue")
|
||||
|
||||
SHOULD_BUFF_POINTS = GENERALLY_BUFF_POINTS
|
||||
EDGE_BUFFER = 0.5
|
||||
|
||||
def __init__(self,
|
||||
color = None,
|
||||
@ -74,14 +74,26 @@ class Mobject(object):
|
||||
).reshape(self.points.shape)
|
||||
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))
|
||||
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
|
||||
|
||||
def rotate_in_place(self, angle, axis = (0, 0, 1)):
|
||||
def rotate_in_place(self, angle, axis = OUT):
|
||||
center = self.get_center()
|
||||
self.shift(-center)
|
||||
self.rotate(angle, axis)
|
||||
@ -89,12 +101,7 @@ class Mobject(object):
|
||||
return self
|
||||
|
||||
def shift(self, vector):
|
||||
cycle = it.cycle(vector)
|
||||
v = np.array([
|
||||
cycle.next()
|
||||
for x in range(self.points.size)
|
||||
]).reshape(self.points.shape)
|
||||
self.points += v
|
||||
self.points += vector
|
||||
return self
|
||||
|
||||
def wag(self, wag_direction = RIGHT, wag_axis = DOWN,
|
||||
@ -105,7 +112,7 @@ class Mobject(object):
|
||||
alphas = alphas**wag_factor
|
||||
self.points += np.dot(
|
||||
alphas.reshape((len(alphas), 1)),
|
||||
np.array(wag_direction).reshape((1, 3))
|
||||
np.array(wag_direction).reshape((1, self.DIM))
|
||||
)
|
||||
return self
|
||||
|
||||
@ -114,18 +121,18 @@ class Mobject(object):
|
||||
return self
|
||||
|
||||
#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)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
corner in the 2d plane.
|
||||
"""
|
||||
shift_val = [0, 0, 0]
|
||||
shift_val = ORIGIN
|
||||
space_dim = (SPACE_WIDTH, SPACE_HEIGHT)
|
||||
for i in [0, 1]:
|
||||
if direction[i] == 0:
|
||||
@ -137,7 +144,6 @@ class Mobject(object):
|
||||
self.shift(shift_val)
|
||||
return self
|
||||
|
||||
|
||||
def scale(self, scale_factor):
|
||||
self.points *= scale_factor
|
||||
return self
|
||||
@ -164,18 +170,6 @@ class Mobject(object):
|
||||
def stretch_to_fit_height(self, height):
|
||||
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):
|
||||
self.rotate(np.pi / 7)
|
||||
self.rotate(np.pi / 7, [1, 0, 0])
|
||||
@ -229,10 +223,12 @@ class Mobject(object):
|
||||
"""
|
||||
function is any map from R^3 to R
|
||||
"""
|
||||
self.points = np.array(sorted(
|
||||
self.points,
|
||||
lambda *points : cmp(*map(function, points))
|
||||
))
|
||||
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]
|
||||
return self
|
||||
|
||||
### Getters ###
|
||||
@ -249,22 +245,24 @@ class Mobject(object):
|
||||
def get_boundary_point(self, 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[dim] = max_or_min_func(self.points[:,dim])
|
||||
return result
|
||||
|
||||
def get_top(self):
|
||||
return self.get_edge_center(1, np.max)
|
||||
return self.get_edge_center(UP)
|
||||
|
||||
def get_bottom(self):
|
||||
return self.get_edge_center(1, np.min)
|
||||
return self.get_edge_center(DOWN)
|
||||
|
||||
def get_right(self):
|
||||
return self.get_edge_center(0, np.max)
|
||||
return self.get_edge_center(RIGHT)
|
||||
|
||||
def get_left(self):
|
||||
return self.get_edge_center(0, np.min)
|
||||
return self.get_edge_center(LEFT)
|
||||
|
||||
def get_width(self):
|
||||
return np.max(self.points[:, 0]) - np.min(self.points[:, 0])
|
||||
@ -341,4 +339,60 @@ class CompoundMobject(Mobject):
|
||||
curr += num_points
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -6,66 +6,22 @@ from image_mobject import text_mobject
|
||||
from constants import *
|
||||
from helpers import *
|
||||
|
||||
|
||||
class Point(Mobject):
|
||||
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)
|
||||
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):
|
||||
self.add_points([
|
||||
[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]
|
||||
])
|
||||
self.add_points(self.location.reshape((1, 3)))
|
||||
|
||||
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
|
||||
DEFAULT_COLOR = "white"
|
||||
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)
|
||||
if center.size == 1:
|
||||
raise Exception("Center must have 2 or 3 coordinates!")
|
||||
@ -94,15 +50,32 @@ class Cross(Mobject1D):
|
||||
])
|
||||
|
||||
class Line(Mobject1D):
|
||||
def __init__(self, start, end, density = DEFAULT_POINT_DENSITY_1D, *args, **kwargs):
|
||||
self.start = np.array(start)
|
||||
self.end = np.array(end)
|
||||
density *= max(self.get_length(), 0.1)
|
||||
MIN_DENSITY = 0.1
|
||||
def __init__(self, start, end, density = DEFAULT_POINT_DENSITY_1D,
|
||||
*args, **kwargs):
|
||||
self.set_start_and_end(start, end)
|
||||
density *= max(self.get_length(), self.MIN_DENSITY)
|
||||
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):
|
||||
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)
|
||||
])
|
||||
|
||||
@ -116,7 +89,7 @@ class Line(Mobject1D):
|
||||
]
|
||||
return rise/run
|
||||
|
||||
class NewArrow(Line):
|
||||
class Arrow(Line):
|
||||
DEFAULT_COLOR = "white"
|
||||
DEFAULT_TIP_LENGTH = 0.25
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -128,22 +101,38 @@ class NewArrow(Line):
|
||||
self.add_tip(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):
|
||||
def __init__(self, start, end, via = None, *args, **kwargs):
|
||||
self.set_start_and_end(start, end)
|
||||
if via == None:
|
||||
via = rotate_vector(
|
||||
end - start,
|
||||
self.via = rotate_vector(
|
||||
self.end - self.start,
|
||||
np.pi/3, [0,0,1]
|
||||
) + start
|
||||
) + self.start
|
||||
elif isinstance(via, Mobject):
|
||||
self.via = via.get_center()
|
||||
else:
|
||||
self.via = via
|
||||
Line.__init__(self, start, end, *args, **kwargs)
|
||||
|
||||
def generate_points(self):
|
||||
self.add_points([
|
||||
4*(0.25-t*(1-t))*(t*self.end + (1-t)*self.start) +
|
||||
4*t*(1-t)*self.via
|
||||
interpolate(
|
||||
interpolate(self.start, self.end, t),
|
||||
self.via,
|
||||
t*(1-t)
|
||||
)
|
||||
for t in np.arange(0, 1, self.epsilon)
|
||||
])
|
||||
|
||||
|
@ -10,20 +10,14 @@ from animation import *
|
||||
from mobject import *
|
||||
from constants import *
|
||||
from region import *
|
||||
from scene import Scene, RearrangeEquation
|
||||
from scene import Scene
|
||||
from script_wrapper import command_line_create_scene
|
||||
|
||||
|
||||
class SampleScene(RearrangeEquation):
|
||||
class SampleScene(Scene):
|
||||
def construct(self):
|
||||
three = tex_mobject("3")
|
||||
three.sort_points(np.linalg.norm)
|
||||
self.animate(DelayByOrder(ApplyMethod(three.scale, 3)))
|
||||
|
||||
self.dither()
|
||||
|
||||
|
||||
|
||||
self.add(three)
|
||||
|
||||
if __name__ == "__main__":
|
||||
command_line_create_scene()
|
@ -19,7 +19,7 @@ def tex_to_image(expression,
|
||||
image_dir = os.path.join(TEX_IMAGE_DIR, exp_hash)
|
||||
if os.path.exists(image_dir):
|
||||
result = [
|
||||
Image.open(os.path.join(image_dir, png_file)).convert('RGB')
|
||||
os.path.join(image_dir, png_file)
|
||||
for png_file in sorted(
|
||||
os.listdir(image_dir),
|
||||
cmp_enumerated_files
|
||||
@ -92,7 +92,7 @@ def dvi_to_png(filename, regen_if_exists = False):
|
||||
if name.endswith(".png")
|
||||
]
|
||||
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")
|
||||
|
||||
def cmp_enumerated_files(name1, name2):
|
||||
|
Reference in New Issue
Block a user