diff --git a/camera.py b/camera.py index 6c66a1e3..412d59f2 100644 --- a/camera.py +++ b/camera.py @@ -84,27 +84,28 @@ class Camera(object): print mobject # raise Exception("I don't know how to display that") - # def display_region(self, region): - # (h, w) = self.pixel_shape - # scalar = 2*self.space_shape[0] / h - # xs = scalar*np.arange(-w/2, w/2)+self.space_center[0] - # ys = -scalar*np.arange(-h/2, h/2)+self.space_center[1] - # x_array = np.dot(np.ones((h, 1)), xs.reshape((1, w))) - # y_array = np.dot(ys.reshape(h, 1), np.ones((1, w))) - # covered = region.condition(x_array, y_array) - # rgb = np.array(Color(region.color).get_rgb()) - # rgb = (255*rgb).astype('uint8') - # self.pixel_array[covered] = rgb + def display_region(self, region): + (h, w) = self.pixel_shape + scalar = 2*self.space_shape[0] / h + xs = scalar*np.arange(-w/2, w/2)+self.space_center[0] + ys = -scalar*np.arange(-h/2, h/2)+self.space_center[1] + x_array = np.dot(np.ones((h, 1)), xs.reshape((1, w))) + y_array = np.dot(ys.reshape(h, 1), np.ones((1, w))) + covered = region.condition(x_array, y_array) + rgb = np.array(Color(region.color).get_rgb()) + rgb = (255*rgb).astype('uint8') + self.pixel_array[covered] = rgb def display_vectorized(self, vect_mobject): + if vect_mobject.is_subpath: + #Subpath vectorized mobjects are taken care + #of by their parent + return im = Image.fromarray(self.pixel_array, mode = "RGB") canvas = aggdraw.Draw(im) pen, fill = self.get_pen_and_fill(vect_mobject) - pathstring = self.get_pathstring( - self.points_to_pixel_coords(vect_mobject.points), - closed = vect_mobject.is_closed() - ) + pathstring = self.get_pathstring(vect_mobject) symbol = aggdraw.Symbol(pathstring) canvas.symbol((0, 0), symbol, pen, fill) canvas.flush() @@ -121,19 +122,23 @@ class Camera(object): ) return (pen, fill) - def get_pathstring(self, cubic_bezier_points, closed = False): - start = "m%d,%d"%tuple(cubic_bezier_points[0]) - #(handle1, handle2, anchor) tripletes - triplets = zip(*[ - cubic_bezier_points[i+1::3] - for i in range(3) - ]) - cubics = [ - "C" + ",".join(map(str, it.chain(*triplet))) - for triplet in triplets - ] - end = "z" if closed else "" - return " ".join([start] + cubics + [end]) + def get_pathstring(self, vect_mobject): + result = "" + for mob in [vect_mobject]+vect_mobject.subpath_mobjects: + points = mob.points + coords = self.points_to_pixel_coords(points) + start = "M%d %d"%tuple(coords[0]) + #(handle1, handle2, anchor) tripletes + triplets = zip(*[ + coords[i+1::3] + for i in range(3) + ]) + cubics = [ + "C" + " ".join(map(str, it.chain(*triplet))) + for triplet in triplets + ] + result += " ".join([start] + cubics) + return result def display_point_cloud(self, points, rgbs, thickness): if len(points) == 0: diff --git a/helpers.py b/helpers.py index d072578c..9b46f4e0 100644 --- a/helpers.py +++ b/helpers.py @@ -9,8 +9,10 @@ import string import re from scipy import linalg +from constants import * -def get_smooth_handle_points(points, closed = False): + +def get_smooth_handle_points(points): num_handles = len(points) - 1 dim = points.shape[1] if num_handles < 1: @@ -30,8 +32,8 @@ def get_smooth_handle_points(points, closed = False): diag[2,1:-2:2] = -2 diag[3,0:-3:2] = 1 #last - diag[2,-2] = 2 - diag[1,-1] = -1 + diag[2,-2] = -1 + diag[1,-1] = 2 #This is the b as in Ax = b, where we are solving for x, #and A is represented using diag. However, think of entries #to x and b as being points in space, not numbers @@ -42,7 +44,7 @@ def get_smooth_handle_points(points, closed = False): solve_func = lambda b : linalg.solve_banded( (l, u), diag, b ) - if closed: + if is_closed(points): #Get equations to relate first and last points matrix = diag_to_matrix((l, u), diag) #last row handles second derivative @@ -74,8 +76,8 @@ def diag_to_matrix(l_and_u, diag): ) return matrix -from constants import * - +def is_closed(points): + return np.all(points[0] == points[-1]) def color_to_rgb(color): return np.array(Color(color).get_rgb()) diff --git a/mobject/vectorized_mobject.py b/mobject/vectorized_mobject.py index e6c78232..b79f74ee 100644 --- a/mobject/vectorized_mobject.py +++ b/mobject/vectorized_mobject.py @@ -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