Vectorize all the things

This commit is contained in:
Grant Sanderson
2016-04-17 00:31:38 -07:00
parent bd3783586a
commit 0d4e928b6e
12 changed files with 406 additions and 362 deletions

View File

@ -65,8 +65,8 @@ class Camera(object):
def capture_mobject(self, mobject):
return self.capture_mobjects([mobject])
def capture_mobjects(self, mobjects, include_sub_mobjects = True):
if include_sub_mobjects:
def capture_mobjects(self, mobjects, include_submobjects = True):
if include_submobjects:
mobjects = it.chain(*[
mob.nonempty_family_members()
for mob in mobjects
@ -137,7 +137,8 @@ class Camera(object):
"C" + " ".join(map(str, it.chain(*triplet)))
for triplet in triplets
]
result += " ".join([start] + cubics)
end = "Z" if vmobject.mark_paths_closed else ""
result += " ".join([start] + cubics + [end])
return result
def display_point_cloud(self, points, rgbs, thickness):

View File

@ -69,12 +69,10 @@ for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TEX_DIR,
if not os.path.exists(folder):
os.mkdir(folder)
PDF_DENSITY = 800
SIZE_TO_REPLACE = "SizeHere"
TEX_TEXT_TO_REPLACE = "YourTextHere"
TEMPLATE_TEX_FILE = os.path.join(THIS_DIR, "template.tex")
TEMPLATE_TEXT_FILE = os.path.join(THIS_DIR, "text_template.tex")
MAX_LEN_FOR_HUGE_TEX_FONT = 25
LOGO_PATH = os.path.join(IMAGE_DIR, "logo.png")

View File

@ -87,10 +87,10 @@ def color_to_int_rgb(color):
def compass_directions(n = 4, start_vect = UP):
angle = 2*np.pi/n
return [
return np.array([
rotate_vector(start_vect, k*angle)
for k in range(n)
]
])
def partial_bezier_points(points, a, b):
"""

View File

@ -23,9 +23,9 @@ class Mobject(object):
"dim" : 3,
"target" : None
}
def __init__(self, *sub_mobjects, **kwargs):
def __init__(self, *submobjects, **kwargs):
digest_config(self, kwargs)
self.sub_mobjects = list(sub_mobjects)
self.submobjects = list(submobjects)
self.color = Color(self.color)
if self.name is None:
self.name = self.__class__.__name__
@ -48,7 +48,7 @@ class Mobject(object):
pass
def add(self, *mobjects):
self.sub_mobjects = list_update(self.sub_mobjects, mobjects)
self.submobjects = list_update(self.submobjects, mobjects)
return self
def get_array_attrs(self):
@ -57,13 +57,13 @@ class Mobject(object):
def digest_mobject_attrs(self):
"""
Ensures all attributes which are mobjects are included
in the sub_mobjects list.
in the submobjects list.
"""
mobject_attrs = filter(
lambda x : isinstance(x, Mobject),
self.__dict__.values()
)
self.sub_mobjects = list_update(self.sub_mobjects, mobject_attrs)
self.submobjects = list_update(self.submobjects, mobject_attrs)
return self
def apply_over_attr_arrays(self, func):
@ -229,7 +229,7 @@ class Mobject(object):
return self.scale(height/self.get_height())
def replace(self, mobject, stretch = False):
if not mobject.get_num_points() and not mobject.sub_mobjects:
if not mobject.get_num_points() and not mobject.submobjects:
raise Warning("Attempting to replace mobject with no points")
return self
if stretch:
@ -302,7 +302,7 @@ class Mobject(object):
values = []
values += [
mob.reduce_across_dimension(points_func, reduce_func, dim)
for mob in self.sub_mobjects
for mob in self.submobjects
]
try:
return reduce_func(values)
@ -388,10 +388,10 @@ class Mobject(object):
def split(self):
result = [self] if len(self.points) > 0 else []
return result + self.sub_mobjects
return result + self.submobjects
def submobject_family(self):
sub_families = map(Mobject.submobject_family, self.sub_mobjects)
sub_families = map(Mobject.submobject_family, self.submobjects)
all_mobjects = [self] + reduce(op.add, sub_families, [])
return remove_list_redundancies(all_mobjects)
@ -403,10 +403,10 @@ class Mobject(object):
## Alignment
def align_data(self, mobject):
self.align_sub_mobjects(mobject)
self.align_submobjects(mobject)
self.align_points(mobject)
#Recurse
for m1, m2 in zip(self.sub_mobjects, mobject.sub_mobjects):
for m1, m2 in zip(self.submobjects, mobject.submobjects):
m1.align_data(m2)
def get_point_mobject(self, center = None):
@ -429,7 +429,7 @@ class Mobject(object):
def align_points_with_larger(self, larger_mobject):
raise Exception("Not implemented")
def align_sub_mobjects(self, mobject):
def align_submobjects(self, mobject):
#If one is empty, and the other is not,
#push it into its submobject list
self_has_points, mob_has_points = [
@ -437,30 +437,31 @@ class Mobject(object):
for mob in self, mobject
]
if self_has_points and not mob_has_points:
self.push_self_into_sub_mobjects()
self.push_self_into_submobjects()
elif mob_has_points and not self_has_points:
mob.push_self_into_sub_mobjects()
self_count = len(self.sub_mobjects)
mob_count = len(mobject.sub_mobjects)
mob.push_self_into_submobjects()
self_count = len(self.submobjects)
mob_count = len(mobject.submobjects)
diff = abs(self_count-mob_count)
if self_count < mob_count:
self.add_n_more_sub_mobjects(diff)
self.add_n_more_submobjects(diff)
elif mob_count < self_count:
mobject.add_n_more_sub_mobjects(diff)
mobject.add_n_more_submobjects(diff)
return self
def push_self_into_sub_mobjects(self):
def push_self_into_submobjects(self):
copy = self.copy()
copy.sub_mobjects = []
copy.submobjects = []
self.points = np.zeros((0, self.dim))
self.add(copy)
return self
def add_n_more_sub_mobjects(self, n):
if n > 0 and len(self.sub_mobjects) == 0:
def add_n_more_submobjects(self, n):
if n > 0 and len(self.submobjects) == 0:
self.add(self.copy())
n = n-1
for i in range(n):
self.add(self.sub_mobjects[i].copy())
self.add(self.submobjects[i].copy())
return self
def interpolate(self, mobject1, mobject2, alpha, path_func):

View File

@ -88,19 +88,19 @@ class PMobject(Mobject):
def fade_to(self, color, alpha):
self.rgbs = interpolate(self.rgbs, np.array(Color(color).rgb), alpha)
for mob in self.sub_mobjects:
for mob in self.submobjects:
mob.fade_to(color, alpha)
return self
def get_all_rgbs(self):
return self.get_merged_array("rgbs")
def ingest_sub_mobjects(self):
def ingest_submobjects(self):
attrs = self.get_array_attrs()
arrays = map(self.get_merged_array, attrs)
for attr, array in zip(attrs, arrays):
setattr(self, attr, array)
self.sub_mobjects = []
self.submobjects = []
return self
def get_color(self):

196
mobject/svg_mobject.py Normal file
View File

@ -0,0 +1,196 @@
from xml.dom import minidom
import warnings
from vectorized_mobject import VMobject
from topics.geometry import Rectangle, Circle
from helpers import *
SVG_SCALE_VALUE = 0.1
class SVGMobject(VMobject):
CONFIG = {
"stroke_width" : 0,
"fill_opacity" : 1.0,
"fill_color" : WHITE, #TODO...
}
def __init__(self, svg_file, **kwargs):
digest_config(self, kwargs, locals())
VMobject.__init__(self, **kwargs)
def generate_points(self):
doc = minidom.parse(self.svg_file)
defs = doc.getElementsByTagName("defs")[0]
g = doc.getElementsByTagName("g")[0]
ref_to_mob = self.get_ref_to_mobject_map(defs)
for element in g.childNodes:
if not isinstance(element, minidom.Element):
continue
mob = None
if element.tagName == 'use':
mob = self.use_to_mobject(element, ref_to_mob)
elif element.tagName == 'rect':
mob = self.rect_to_mobject(element)
elif element.tagName == 'circle':
mob = self.circle_to_mobject(element)
else:
warnings.warn("Unknown element type: " + element.tagName)
if mob is not None:
self.add(mob)
doc.unlink()
self.move_into_position()
self.organize_submobjects()
def use_to_mobject(self, use_element, ref_to_mob):
#Remove initial "#" character
ref = use_element.getAttribute("xlink:href")[1:]
try:
mob = ref_to_mob[ref]
except:
warnings.warn("%s not recognized"%ref)
return
if mob in self.submobjects:
mob = VMobjectFromSVGPathstring(
mob.get_original_path_string()
)
self.handle_transform(use_element, mob)
self.handle_shift(use_element, mob)
return mob
def circle_to_mobject(self, circle_element):
pass
def rect_to_mobject(self, rect_element):
if rect_element.hasAttribute("fill"):
if Color(str(rect_element.getAttribute("fill"))) == Color(WHITE):
return
mob = Rectangle(
width = float(rect_element.getAttribute("width")),
height = float(rect_element.getAttribute("height")),
stroke_width = 0,
fill_color = WHITE,
fill_opacity = 1.0
)
self.handle_shift(rect_element, mob)
mob.shift(mob.get_center()-mob.get_corner(DOWN+LEFT))
return mob
def handle_shift(self, element, mobject):
x, y = 0, 0
if element.hasAttribute('x'):
x = float(element.getAttribute('x'))
if element.hasAttribute('y'):
#Flip y
y = -float(element.getAttribute('y'))
mobject.shift(x*RIGHT+y*UP)
def handle_transform(self, element, mobject):
pass
def move_into_position(self):
self.center()
self.scale(SVG_SCALE_VALUE)
self.init_colors()
def organize_submobjects(self):
self.submobjects.sort(
lambda m1, m2 : int((m1.get_left()-m2.get_left())[0])
)
def get_ref_to_mobject_map(self, defs):
ref_to_mob = {}
for element in defs.childNodes:
if not isinstance(element, minidom.Element):
continue
ref = element.getAttribute('id')
if element.tagName == "path":
path_string = element.getAttribute('d')
mob = VMobjectFromSVGPathstring(path_string)
ref_to_mob[ref] = mob
if element.tagName == "use":
ref_to_mob[ref] = self.use_to_mobject(element, ref_to_mob)
return ref_to_mob
class VMobjectFromSVGPathstring(VMobject):
def __init__(self, path_string, **kwargs):
digest_locals(self)
VMobject.__init__(self, **kwargs)
def get_path_commands(self):
return [
"M", #moveto
"L", #lineto
"H", #horizontal lineto
"V", #vertical lineto
"C", #curveto
"S", #smooth curveto
"Q", #quadratic Bezier curve
"T", #smooth quadratic Bezier curveto
"A", #elliptical Arc
"Z", #closepath
]
def generate_points(self):
pattern = "[%s]"%("".join(self.get_path_commands()))
pairs = zip(
re.findall(pattern, self.path_string),
re.split(pattern, self.path_string)[1:]
)
#Which mobject should new points be added to
self.growing_path = self
for command, coord_string in pairs:
self.handle_command(command, coord_string)
#people treat y-coordinate differently
self.rotate(np.pi, RIGHT)
def handle_command(self, command, coord_string):
#new_points are the points that will be added to the curr_points
#list. This variable may get modified in the conditionals below.
points = self.growing_path.points
new_points = self.string_to_points(coord_string)
if command == "M": #moveto
if len(points) > 0:
self.add_subpath(new_points)
self.growing_path = self.subpath_mobjects[-1]
else:
self.growing_path.start_at(new_points[0])
return
elif command in ["L", "H", "V"]: #lineto
if command == "H":
new_points[0,1] = points[-1,1]
elif command == "V":
new_points[0,1] = new_points[0,0]
new_points[0,0] = points[-1,0]
new_points = new_points[[0, 0, 0]]
elif command == "C": #curveto
pass #Yay! No action required
elif command in ["S", "T"]: #smooth curveto
handle1 = points[-1]+(points[-1]-points[-2])
new_points = np.append([handle1], new_points, axis = 0)
if command in ["Q", "T"]: #quadratic Bezier curve
#TODO, this is a suboptimal approximation
new_points = np.append([new_points[0]], new_points, axis = 0)
elif command == "A": #elliptical Arc
raise Exception("Not implemented")
elif command == "Z": #closepath
if not is_closed(points):
#Both handles and new anchor are the start
new_points = points[[0, 0, 0]]
self.growing_path.add_control_points(new_points)
def string_to_points(self, coord_string):
numbers = [
float(s)
for s in coord_string.split(" ")
if s != ""
]
if len(numbers)%2 == 1:
numbers.append(0)
num_points = len(numbers)/2
result = np.zeros((num_points, self.dim))
result[:,:2] = np.array(numbers).reshape((num_points, 2))
return result
def get_original_path_string(self):
return self.path_string

View File

@ -1,11 +1,7 @@
from mobject import Mobject
from point_cloud_mobject import PMobject
from image_mobject import ImageMobject
from svg_mobject import SVGMobject
from helpers import *
#TODO, Cleanup and refactor this file.
class TexMobject(PMobject):
class TexMobject(SVGMobject):
CONFIG = {
"template_tex_file" : TEMPLATE_TEX_FILE,
"color" : WHITE,
@ -13,32 +9,17 @@ class TexMobject(PMobject):
"should_center" : True,
}
def __init__(self, expression, **kwargs):
if "size" not in kwargs:
size = "\\Large"
digest_locals(self)
Mobject.__init__(self, **kwargs)
def generate_points(self):
image_files = tex_to_image_files(
digest_config(self, kwargs, locals())
image_file = tex_to_image_file(
self.expression,
self.size,
self.template_tex_file
)
for image_file in image_files:
self.add(ImageMobject(image_file, should_center = False))
if len(image_files) == 1:
## Single image should be the mobject, not a sub_mobject
self.ingest_sub_mobjects()
if self.should_center:
self.center()
self.highlight(self.color)
SVGMobject.__init__(self, image_file, **kwargs)
class TextMobject(TexMobject):
CONFIG = {
"template_tex_file" : TEMPLATE_TEXT_FILE,
"size" : "\\Large", #TODO, auto-adjust?
}
@ -58,43 +39,36 @@ class Brace(TexMobject):
for mob in mobject, self:
mob.rotate(angle)
def tex_hash(expression):
return str(hash(expression))
def tex_hash(expression, size):
return str(hash("".join(expression) + size))
def tex_to_image_files(expression, size, template_tex_file):
"""
Returns list of images for correpsonding with a list of expressions
"""
image_dir = os.path.join(TEX_IMAGE_DIR, tex_hash(expression, size))
def tex_to_image_file(expression, template_tex_file):
image_dir = os.path.join(TEX_IMAGE_DIR, tex_hash(expression))
if os.path.exists(image_dir):
return get_sorted_image_list(image_dir)
tex_file = generate_tex_file(expression, size, template_tex_file)
dvi_file = tex_to_dvi(tex_file)
return dvi_to_png(dvi_file)
tex_file = generate_tex_file(expression, template_tex_file)
pdf_file = tex_to_pdf(tex_file)
return pdf_to_svg(pdf_file)
def generate_tex_file(expression, size, template_tex_file):
if isinstance(expression, list):
expression = tex_expression_list_as_string(expression)
result = os.path.join(TEX_DIR, tex_hash(expression, size))+".tex"
def generate_tex_file(expression, template_tex_file):
result = os.path.join(TEX_DIR, tex_hash(expression))+".tex"
if not os.path.exists(result):
print "Writing \"%s\" at size %s to %s"%(
"".join(expression), size, result
print "Writing \"%s\" to %s"%(
"".join(expression), result
)
with open(template_tex_file, "r") as infile:
body = infile.read()
body = body.replace(SIZE_TO_REPLACE, size)
body = body.replace(TEX_TEXT_TO_REPLACE, expression)
with open(result, "w") as outfile:
outfile.write(body)
return result
def tex_to_dvi(tex_file):
result = tex_file.replace(".tex", ".dvi")
if not os.path.exists(result):
def tex_to_pdf(tex_file):
result = tex_file.replace(".tex", ".pdf")
if not os.path.exists(result) or True:
commands = [
"latex",
"pdflatex",
"-interaction=batchmode",
"-output-directory=" + TEX_DIR,
tex_file,
@ -104,36 +78,42 @@ def tex_to_dvi(tex_file):
os.system(" ".join(commands))
return result
def tex_expression_list_as_string(expression):
return "".join([
"\onslide<%d>{"%count + exp + "}"
for count, exp in zip(it.count(1), expression)
])
def dvi_to_png(dvi_file, regen_if_exists = False):
def pdf_to_svg(pdf_file, regen_if_exists = False):
"""
Converts a dvi, which potentially has multiple slides, into a
directory full of enumerated pngs corresponding with these slides.
Returns a list of PIL Image objects for these images sorted as they
where in the dvi
"""
directory, filename = os.path.split(dvi_file)
name = filename.replace(".dvi", "")
images_dir = os.path.join(TEX_IMAGE_DIR, name)
if not os.path.exists(images_dir):
os.mkdir(images_dir)
if os.listdir(images_dir) == [] or regen_if_exists:
result = pdf_file.replace(".pdf", ".svg")
if not os.path.exists(result) or True:
commands = [
"convert",
"-density",
str(PDF_DENSITY),
dvi_file,
"-size",
str(DEFAULT_WIDTH) + "x" + str(DEFAULT_HEIGHT),
os.path.join(images_dir, name + ".png")
"pdf2svg",
pdf_file,
result,
# "> /dev/null"
]
os.system(" ".join(commands))
return get_sorted_image_list(images_dir)
return result
# directory, filename = os.path.split(pdf_file)
# name = filename.replace(".dvi", "")
# images_dir = os.path.join(TEX_IMAGE_DIR, name)
# if not os.path.exists(images_dir):
# os.mkdir(images_dir)
# if os.listdir(images_dir) == [] or regen_if_exists:
# commands = [
# "convert",
# "-density",
# str(PDF_DENSITY),
# pdf_file,
# "-size",
# str(DEFAULT_WIDTH) + "x" + str(DEFAULT_HEIGHT),
# os.path.join(images_dir, name + ".png")
# ]
# os.system(" ".join(commands))
# return get_sorted_image_list(images_dir)
def get_sorted_image_list(images_dir):

View File

@ -11,7 +11,8 @@ class VMobject(Mobject):
#Indicates that it will not be displayed, but
#that it should count in parent mobject's path
"is_subpath" : False,
"closed" : True,
"close_new_points" : True,
"mark_paths_closed" : False,
}
def __init__(self, *args, **kwargs):
self.subpath_mobjects = []
@ -90,9 +91,11 @@ class VMobject(Mobject):
def set_points_as_corners(self, points):
if len(points) <= 1:
return self
handles1 = points[:-1]
handles2 = points[1:]
self.set_anchors_and_handles(points, handles1, handles2)
points = np.array(points)
self.set_anchors_and_handles(points, *[
interpolate(points[:-1], points[1:], alpha)
for alpha in 1./3, 2./3
])
return self
def set_points_smoothly(self, points):
@ -109,7 +112,7 @@ class VMobject(Mobject):
def set_anchor_points(self, points, mode = "smooth"):
if not isinstance(points, np.ndarray):
points = np.array(points)
if self.closed and not is_closed(points):
if self.close_new_points and not is_closed(points):
points = np.append(points, [points[0]], axis = 0)
if mode == "smooth":
self.set_points_smoothly(points)
@ -261,86 +264,6 @@ class VectorizedPoint(VMobject):
VMobject.__init__(self, **kwargs)
self.set_points([location])
class VMobjectFromSVGPathstring(VMobject):
def __init__(self, path_string, **kwargs):
digest_locals(self)
VMobject.__init__(self, **kwargs)
def get_path_commands(self):
return [
"M", #moveto
"L", #lineto
"H", #horizontal lineto
"V", #vertical lineto
"C", #curveto
"S", #smooth curveto
"Q", #quadratic Bezier curve
"T", #smooth quadratic Bezier curveto
"A", #elliptical Arc
"Z", #closepath
]
def generate_points(self):
pattern = "[%s]"%("".join(self.get_path_commands()))
pairs = zip(
re.findall(pattern, self.path_string),
re.split(pattern, self.path_string)[1:]
)
#Which mobject should new points be added to
self.growing_path = self
for command, coord_string in pairs:
self.handle_command(command, coord_string)
#people treat y-coordinate differently
self.rotate(np.pi, RIGHT)
def handle_command(self, command, coord_string):
#new_points are the points that will be added to the curr_points
#list. This variable may get modified in the conditionals below.
points = self.growing_path.points
new_points = self.string_to_points(coord_string)
if command == "M": #moveto
if len(points) > 0:
self.add_subpath(new_points)
self.growing_path = self.subpath_mobjects[-1]
else:
self.growing_path.start_at(new_points[0])
return
elif command in ["L", "H", "V"]: #lineto
if command == "H":
new_points[0,1] = points[-1,1]
elif command == "V":
new_points[0,1] = new_points[0,0]
new_points[0,0] = points[-1,0]
new_points = new_points[[0, 0, 0]]
elif command == "C": #curveto
pass #Yay! No action required
elif command in ["S", "T"]: #smooth curveto
handle1 = points[-1]+(points[-1]-points[-2])
new_points = np.append([handle1], new_points, axis = 0)
if command in ["Q", "T"]: #quadratic Bezier curve
#TODO, this is a suboptimal approximation
new_points = np.append([new_points[0]], new_points, axis = 0)
elif command == "A": #elliptical Arc
raise Exception("Not implemented")
elif command == "Z": #closepath
if not is_closed(points):
#Both handles and new anchor are the start
new_points = points[[0, 0, 0]]
self.growing_path.add_control_points(new_points)
def string_to_points(self, coord_string):
numbers = [
float(s)
for s in coord_string.split(" ")
if s is not ""
]
if len(numbers)%2 == 1:
numbers.append(0)
num_points = len(numbers)/2
result = np.zeros((num_points, self.dim))
result[:,:2] = np.array(numbers).reshape((num_points, 2))
return result

View File

@ -153,7 +153,7 @@ class Scene(object):
self.separate_moving_and_static_mobjects(*animations)
self.update_frame(
static_mobjects,
include_sub_mobjects = False
include_submobjects = False
)
static_image = self.get_frame()

View File

@ -21,7 +21,6 @@
\centering
\begin{frame}
SizeHere
\begin{align*}
YourTextHere
\end{align*}

View File

@ -21,7 +21,6 @@
\begin{document}
\begin{frame}
SizeHere
YourTextHere
\end{frame}

View File

@ -1,58 +1,69 @@
from helpers import *
from mobject import Mobject, Mobject1D, Point
from mobject.vectorized_mobject import VMobject
class Dot(Mobject1D): #Use 1D density, even though 2D
class Arc(VMobject):
CONFIG = {
"radius" : 0.05
"radius" : 1.0,
"start_angle" : 0,
"close_new_points" : False,
"num_anchors" : 8,
"anchors_span_full_range" : True
}
def __init__(self, center_point = ORIGIN, **kwargs):
def __init__(self, angle, **kwargs):
digest_locals(self)
Mobject1D.__init__(self, **kwargs)
VMobject.__init__(self, **kwargs)
def generate_points(self):
self.add_points([
np.array((t*np.cos(theta), t*np.sin(theta), 0)) + self.center_point
for t in np.arange(self.epsilon, self.radius, self.epsilon)
for new_epsilon in [2*np.pi*self.epsilon*self.radius/t]
for theta in np.arange(0, 2 * np.pi, new_epsilon)
])
self.set_anchor_points(
self.get_unscaled_anchor_points(),
mode = "smooth"
)
self.scale(self.radius)
class Cross(Mobject1D):
def get_unscaled_anchor_points(self):
step = self.angle/self.num_anchors
end_angle = self.start_angle + self.angle
if self.anchors_span_full_range:
end_angle += step
return [
np.cos(a)*RIGHT+np.sin(a)*UP
for a in np.arange(
self.start_angle, end_angle, step
)
]
class Circle(Arc):
CONFIG = {
"color" : YELLOW,
"radius" : 0.3
"color" : RED,
"close_new_points" : True,
"anchors_span_full_range" : False
}
def __init__(self, center_point = ORIGIN, **kwargs):
digest_locals(self)
Mobject1D.__init__(self, **kwargs)
def __init__(self, **kwargs):
Arc.__init__(self, 2*np.pi, **kwargs)
def generate_points(self):
self.add_points([
(sgn * x, x, 0)
for x in np.arange(-self.radius / 2, self.radius/2, self.epsilon)
for sgn in [-1, 1]
])
self.shift(self.center_point)
class Line(Mobject1D):
class Dot(Circle): #Use 1D density, even though 2D
CONFIG = {
"buff" : 0
"radius" : 0.05,
"stroke_width" : 0,
"fill_color" : WHITE,
"fill_opacity" : 1.0
}
class Line(VMobject):
CONFIG = {
"buff" : 0,
"close_new_points" : False,
}
def __init__(self, start, end, **kwargs):
digest_config(self, kwargs)
self.set_start_and_end(start, end)
Mobject1D.__init__(self, **kwargs)
VMobject.__init__(self, **kwargs)
def set_start_and_end(self, start, end):
preliminary_start, preliminary_end = [
arg.get_center()
if isinstance(arg, Mobject)
else np.array(arg).astype('float')
for arg in start, end
]
start_to_end = preliminary_end - preliminary_start
start_to_end = self.pointify(end) - self.pointify(start)
vect = np.zeros(len(start_to_end))
longer_dim = np.argmax(map(abs, start_to_end))
vect[longer_dim] = start_to_end[longer_dim]
@ -69,8 +80,14 @@ class Line(Mobject1D):
self.start = self.start + self.buff*start_to_end
self.end = self.end - self.buff*start_to_end
def pointify(self, mob_or_point):
if isinstance(mob_or_point, Mobject):
return mob_or_point.get_center()
else:
return np.array(mob_or_point)
def generate_points(self):
self.add_line(self.start, self.end)
self.set_points_as_corners([self.start, self.end])
def get_length(self):
return np.linalg.norm(self.start - self.end)
@ -98,36 +115,26 @@ class Arrow(Line):
}
def __init__(self, *args, **kwargs):
if len(args) == 1:
target = args[0]
if isinstance(target, Mobject):
point = target.get_center()
else:
point = target
point = self.pointify(args[0])
args = (point+UP+LEFT, target)
Line.__init__(self, *args, **kwargs)
self.add_tip()
def add_tip(self):
num_points = self.get_num_points()
vect = self.start-self.end
length = np.linalg.norm(vect)
vect = vect*self.tip_length/length
self.add_points([
interpolate(self.end, self.end+v, t)
for t in np.arange(0, 1, self.tip_length*self.epsilon)
for v in [
rotate_vector(vect, np.pi/4, axis)
for axis in IN, OUT
vect *= self.tip_length/length
tip_points = [
self.end+rotate_vector(vect, u*np.pi/5)
for u in 1, -1
]
])
self.num_tip_points = self.get_num_points()-num_points
def remove_tip(self):
if not hasattr(self, "num_tip_points"):
return self
for attr in "points", "rgbs":
setattr(self, attr, getattr(self, attr)[:-self.num_tip_points])
return self
self.tip = VMobject(close_new_points = False)
self.tip.set_anchor_points(
[tip_points[0], self.end, tip_points[1]],
mode = "corners"
)
self.add(self.tip)
self.init_colors()
class Vector(Arrow):
CONFIG = {
@ -135,131 +142,60 @@ class Vector(Arrow):
"buff" : 0,
}
def __init__(self, start, direction, **kwargs):
if isinstance(start, Mobject):
end = start.get_center()+direction
else:
end = start + direction
Arrow.__init__(self, start, end, **kwargs)
class CurvedLine(Line):
def __init__(self, start, end, via = None, **kwargs):
self.set_start_and_end(start, end)
if via == None:
self.via = rotate_vector(
self.end - self.start,
np.pi/3, [0,0,1]
) + self.start
elif isinstance(via, Mobject):
self.via = via.get_center()
else:
self.via = via
Line.__init__(self, start, end, **kwargs)
def generate_points(self):
self.add_points([
interpolate(
interpolate(self.start, self.end, t),
self.via,
t*(1-t)
)
for t in np.arange(0, 1, self.epsilon)
])
class Arc(Mobject1D):
class Cross(VMobject):
CONFIG = {
"radius" : 1.0,
"start_angle" : 0,
"color" : YELLOW,
"radius" : 0.3
}
def __init__(self, angle, **kwargs):
digest_locals(self)
Mobject1D.__init__(self, **kwargs)
def generate_points(self):
sign = 1 if self.angle >= 0 else -1
self.add_points([
(self.radius*np.cos(theta), self.radius*np.sin(theta), 0)
for theta in np.arange(
self.start_angle,
self.start_angle+self.angle,
sign*self.epsilon/self.radius
)
p1, p2, p3, p4 = self.radius * np.array([
UP+LEFT,
DOWN+RIGHT,
UP+RIGHT,
DOWN+LEFT,
])
self.add(Line(p1, p2), Line(p3, p4))
self.init_colors()
class Circle(Arc):
CONFIG = {
"color" : RED,
}
def __init__(self, **kwargs):
Arc.__init__(self, angle = 2*np.pi, **kwargs)
class CubicBezier(VMobject):
def __init__(self, points, **kwargs):
VMobject.__init__(self, **kwargs)
self.set_points(points)
class Polygon(Mobject1D):
class Polygon(VMobject):
CONFIG = {
"color" : GREEN_D,
"edge_colors" : None
"mark_paths_closed" : True
}
def __init__(self, *vertices, **kwargs):
assert len(vertices) > 1
digest_locals(self)
Mobject1D.__init__(self, **kwargs)
VMobject.__init__(self, **kwargs)
def generate_points(self):
if self.edge_colors:
colors = it.cycle(self.edge_colors)
else:
colors = it.cycle([self.color])
self.indices_of_vertices = []
for start, end in adjascent_pairs(self.vertices):
self.indices_of_vertices.append(self.get_num_points())
self.add_line(start, end, color = colors.next())
self.set_anchor_points(self.vertices, mode = "corners")
def get_vertices(self):
return self.vertices[self.indices_of_vertices]
return self.get_anchors_and_handles()[0]
class Grid(Mobject1D):
CONFIG = {
"height" : 6.0,
"width" : 6.0,
}
def __init__(self, rows, columns, **kwargs):
digest_config(self, kwargs, locals())
Mobject1D.__init__(self, **kwargs)
def generate_points(self):
x_step = self.width / self.columns
y_step = self.height / self.rows
for x in np.arange(0, self.width+x_step, x_step):
self.add_line(
[x-self.width/2., -self.height/2., 0],
[x-self.width/2., self.height/2., 0],
)
for y in np.arange(0, self.height+y_step, y_step):
self.add_line(
[-self.width/2., y-self.height/2., 0],
[self.width/2., y-self.height/2., 0]
)
class Rectangle(Grid):
class Rectangle(VMobject):
CONFIG = {
"color" : YELLOW,
"height" : 2.0,
"width" : 4.0,
"mark_paths_closed" : True
}
def __init__(self, **kwargs):
Grid.__init__(self, 1, 1, **kwargs)
def generate_points(self):
hw = [self.height/2.0, self.width/2.0]
self.add_points([
(x, u, 0) if dim==1 else (u, x, 0)
for dim in 0, 1
for u in hw[1-dim], -hw[1-dim]
for x in np.arange(-hw[dim], hw[dim], self.epsilon)
])
y, x = self.height/2, self.width/2
self.set_anchor_points([
x*LEFT+y*UP,
x*RIGHT+y*UP,
x*RIGHT+y*DOWN,
x*LEFT+y*DOWN
], mode = "corners")
class Square(Rectangle):
CONFIG = {
@ -275,18 +211,29 @@ class Square(Rectangle):
)
class FilledRectangle(Mobject1D):
class Grid(VMobject):
CONFIG = {
"color" : GREY,
"height" : 2.0,
"width" : 4.0,
"height" : 6.0,
"width" : 6.0,
}
def __init__(self, rows, columns, **kwargs):
digest_config(self, kwargs, locals())
VMobject.__init__(self, **kwargs)
def generate_points(self):
self.add_points([
(x, y, 0)
for x in np.arange(-self.width/2, self.width/2, self.epsilon)
for y in np.arange(-self.height/2, self.height/2, self.epsilon)
])
x_step = self.width / self.columns
y_step = self.height / self.rows
for x in np.arange(0, self.width+x_step, x_step):
self.add(Line(
[x-self.width/2., -self.height/2., 0],
[x-self.width/2., self.height/2., 0],
))
for y in np.arange(0, self.height+y_step, y_step):
self.add(Line(
[-self.width/2., y-self.height/2., 0],
[self.width/2., y-self.height/2., 0]
))