mirror of
https://github.com/3b1b/manim.git
synced 2025-08-02 02:35:22 +08:00
Vectorize all the things
This commit is contained in:
@ -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):
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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):
|
||||
|
@ -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
196
mobject/svg_mobject.py
Normal 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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -21,7 +21,6 @@
|
||||
\centering
|
||||
|
||||
\begin{frame}
|
||||
SizeHere
|
||||
\begin{align*}
|
||||
YourTextHere
|
||||
\end{align*}
|
||||
|
@ -21,7 +21,6 @@
|
||||
\begin{document}
|
||||
|
||||
\begin{frame}
|
||||
SizeHere
|
||||
YourTextHere
|
||||
\end{frame}
|
||||
|
||||
|
@ -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]
|
||||
))
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user