Introduced notion of a subpath

This commit is contained in:
Grant Sanderson
2016-04-12 21:57:53 -07:00
parent 9fb31e01d0
commit c685211484
3 changed files with 131 additions and 142 deletions

View File

@ -1,4 +1,4 @@
import re
from .mobject import Mobject
@ -6,10 +6,15 @@ from helpers import *
class VectorizedMobject(Mobject):
CONFIG = {
"closed" : False,
"fill_color" : BLACK,
"fill_opacity" : 0.0
"fill_color" : BLACK,
"fill_opacity" : 0.0,
#Indicates that it will not be displayed, but
#that it should count in parent mobject's path
"is_subpath" : False,
}
def __init__(self, *args, **kwargs):
self.subpath_mobjects = []
Mobject.__init__(self, *args, **kwargs)
## Colors
def init_colors(self):
@ -51,17 +56,6 @@ class VectorizedMobject(Mobject):
self.points[0] = point
return self
def close(self):
self.closed = True
return self
def open(self):
self.closed = False
return self
def is_closed(self):
return self.closed
def add_point(self, handle1, handle2, point):
self.points = np.append(
self.points,
@ -70,6 +64,9 @@ class VectorizedMobject(Mobject):
)
return self
def is_closed(self):
return is_closed(self.points)
def set_anchors_and_handles(self, anchors, handles1, handles2):
assert(len(anchors) == len(handles1)+1)
assert(len(anchors) == len(handles2)+1)
@ -84,7 +81,6 @@ class VectorizedMobject(Mobject):
def set_points_as_corners(self, points):
if len(points) <= 1:
return self
points = self.close_if_needed(points)
handles1 = points[:-1]
handles2 = points[1:]
self.set_anchors_and_handles(points, handles1, handles2)
@ -93,28 +89,23 @@ class VectorizedMobject(Mobject):
def set_points_smoothly(self, points):
if len(points) <= 1:
return self
points = self.close_if_needed(points)
h1, h2 = get_smooth_handle_points(points, self.is_closed())
h1, h2 = get_smooth_handle_points(points)
self.set_anchors_and_handles(points, h1, h2)
return self
def close_if_needed(self, points):
if self.is_closed() and not np.all(points[0] == points[-1]):
points = np.append(
points,
[points[0]],
axis = 0
)
return points
def set_points(self, points):
self.points = points
return self
def set_points(self, points, mode = "smooth"):
points = np.array(points)
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):
points = np.append(points, [points[0]], axis = 0)
if mode == "smooth":
self.set_points_smoothly(points)
elif mode == "corners":
self.set_points_as_corners(points)
elif mode == "handles_included":
self.points = points
else:
raise Exception("Unknown mode")
return self
@ -130,6 +121,25 @@ class VectorizedMobject(Mobject):
def make_jagged(self):
return self.change_mode("corners")
def add_subpath(self, points):
"""
A VectorizedMobject is meant to represnt
a single "path", in the svg sense of the word.
However, one such path may really consit of separate
continuous components if there is a move_to command.
These other portions of the path will be treated as submobjects,
but will be tracked in a separate special list for when
it comes time to display.
"""
subpath_mobject = VectorizedMobject(
is_subpath = True
)
subpath_mobject.set_points(points)
self.subpath_mobjects.append(subpath_mobject)
self.add(subpath_mobject)
return self
## Information about line
def component_curves(self):
@ -157,24 +167,6 @@ class VectorizedMobject(Mobject):
## Alignment
# def align_points_with_larger(self, larger_mobject):
# assert(isinstance(larger_mobject, VectorizedMobject))
# anchors, handles1, handles2 = self.get_anchors_and_handles()
# old_n = len(anchors)
# new_n = larger_mobject.get_num_points()
# #Buff up list of anchor points to appropriate length
# new_anchors = anchors[old_n*np.arange(new_n)/new_n]
# #At first, handles are on anchor points
# #the [2:] is because start has no handles
# new_points = new_anchors.repeat(3, axis = 0)[2:]
# #These indices indicate the spots between genuinely
# #different anchor points in new_points list
# indices = 3*(np.arange(old_n) * new_n / old_n)[1:]
# new_points[indices+1] = handles1
# new_points[indices+2] = handles2
# self.set_points(new_points, mode = "handles_included")
# return self
def align_points_with_larger(self, larger_mobject):
assert(isinstance(larger_mobject, VectorizedMobject))
points = np.array([self.points[0]])
@ -213,39 +205,7 @@ class VectorizedMobject(Mobject):
getattr(mobject2, attr),
alpha
))
def get_partial_bezier_points(self, x, pre_x = True):
"""
Input is a number number 0 <= x <= 1, which
corresponds to some point along the curve.
This point lies on some cubic. This function
return the four bezeir points giving a partial
version of that cubic. Either the part before x,
if pre_x == True, otherwise the part after x
"""
if x >= 1.0:
return np.array(4*[self.points[-1]])
num_cubics = self.get_num_points()-1
which_curve = int(x*num_cubics)
alpha = num_cubics*(x%(1./num_cubics))
cubic = self.get_nth_curve(which_curve)
new_anchor = cubic(alpha)
a1, h1, h2, a2 = self.points[3*which_curve:3*which_curve+4]
if pre_x:
return np.array([
a1,
interpolate(cubic(alpha/3), h1, alpha),
interpolate(cubic(2*alpha/3), h2, alpha),
new_anchor
])
else:
return np.array([
new_anchor,
interpolate(h1, cubic((1-alpha)/3), alpha),
interpolate(h2, cubic(2*(1-alpha/3), alpha)),
a2
])
self.closed = mobject1.is_closed() and mobject2.is_closed()
def become_partial(self, mobject, a, b):
@ -255,34 +215,31 @@ class VectorizedMobject(Mobject):
#-A start, which is some ending portion of an inner cubic
#-An end, which is the starting portion of a later inner cubic
self.open()
if a <= 0 and b >= 1 and mobject.is_closed():
self.close()
if a <= 0 and b >= 1:
if mobject.is_closed():
self.close()
self.set_points(mobject.points, "handles_included")
return self
num_cubics = mobject.get_num_points()-1
if a <= 0:
lower_index = 0
start_points = np.zeros((0, 3))
else:
lower_index = 3*int(a*num_cubics)+4
start_points = mobject.get_partial_bezier_points(
a, pre_x = False
)
if b >= 1:
upper_index = len(mobject.points)+1
end_points = np.zeros((0, 3))
else:
upper_index = 3*int(b*num_cubics)
end_points = mobject.get_partial_bezier_points(
b, pre_x = True
)
new_points = reduce(
lambda a, b : np.append(a, b, axis = 0),
[
start_points,
mobject.points[lower_index:upper_index],
end_points
]
lower_index = int(a*num_cubics)
upper_index = int(b*num_cubics)
points = np.array(
mobject.points[3*lower_index:3*upper_index+4]
)
self.set_points(new_points, "handles_included")
if len(points) > 1:
#This is a kind of neat-but-dense algorithm
#for how to interpolate the handle points
a_residue = (num_cubics*a)%1
points[:4] = [
bezier(points[i:4])(a_residue)
for i in range(4)
]
b_residue = (num_cubics*b)%1
points[-4:] = [
bezier(points[-4:len(points)-3+i])(b_residue)
for i in range(4)
]
self.set_points(points, "handles_included")
return self
@ -294,7 +251,32 @@ class VectorizedPoint(VectorizedMobject):
VectorizedMobject.__init__(self, **kwargs)
self.set_points([location])
class VectorizedMobjectFromSVGPathstring(VectorizedMobject):
def __init__(self, path_string, **kwargs):
digest_locals(self)
VectorizedMobject.__init__(self, **kwargs)
def generate_points(self):
path_commands = [
"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
]
pattern = "[%s]"%("".join(path_commands))
pairs = zip(
re.findall(pattern, self.pathstring),
re.split(pattern, self.path_string)
)
for command, coord_string in pairs:
pass
#TODO