diff --git a/animation/transform.py b/animation/transform.py index 361a90d7..12a8d98f 100644 --- a/animation/transform.py +++ b/animation/transform.py @@ -12,8 +12,7 @@ from mobject import Mobject, Point class Transform(Animation): CONFIG = { - "path_func" : straight_path, - "should_black_out_extra_points" : False + "path_func" : straight_path } def __init__(self, mobject, ending_mobject, **kwargs): mobject = instantiate(mobject) @@ -22,58 +21,24 @@ class Transform(Animation): digest_config(self, kwargs, locals()) count1, count2 = mobject.get_num_points(), ending_mobject.get_num_points() if count2 == 0: - ending_mobject.add_points( - [mobject.get_center()], - color = BLACK - ) + ending_mobject = mobject.get_point_mobject() count2 = ending_mobject.get_num_points() - Mobject.align_data(mobject, ending_mobject) - if self.should_black_out_extra_points and count2 < count1: - self.black_out_extra_points(count1, count2) + mobject.align_data(ending_mobject) Animation.__init__(self, mobject, **kwargs) self.name += "To" + str(ending_mobject) - self.mobject.point_thickness = ending_mobject.point_thickness + self.mobject.stroke_width = ending_mobject.stroke_width - def black_out_extra_points(self, count1, count2): - #Ensure redundant pixels fade to black - indices = np.arange( - 0, count1-1, float(count1) / count2 - ).astype('int') - temp = np.zeros(self.ending_mobject.points.shape) - temp[indices] = self.ending_mobject.rgbs[indices] - self.ending_mobject.rgbs = temp - self.non_redundant_m2_indices = indices - def update_mobject(self, alpha): families = map( Mobject.submobject_family, [self.mobject, self.starting_mobject, self.ending_mobject] ) for m, start, end in zip(*families): - # print m, start, end - m.points = self.path_func( - start.points, end.points, alpha - ) - m.rgbs = straight_path(start.rgbs, end.rgbs, alpha) + m.interpolate(start, end, alpha, self.path_func) - def clean_up(self): - Animation.clean_up(self) - if hasattr(self, "non_redundant_m2_indices"): - #Reduce mobject (which has become identical to mobject2), as - #well as mobject2 itself - for mobject in [self.mobject, self.ending_mobject]: - for attr in ['points', 'rgbs']: - setattr( - mobject, attr, - getattr( - self.ending_mobject, - attr - )[self.non_redundant_m2_indices] - ) - class ClockwiseTransform(Transform): CONFIG = { "path_func" : clockwise_path() @@ -219,10 +184,10 @@ class TransformAnimations(Transform): anim.set_run_time(self.run_time) if start_anim.starting_mobject.get_num_points() != end_anim.starting_mobject.get_num_points(): - Mobject.align_data(start_anim.starting_mobject, end_anim.starting_mobject) + start_anim.starting_mobject.align_data(end_anim.starting_mobject) for anim in start_anim, end_anim: if hasattr(anim, "ending_mobject"): - Mobject.align_data(anim.starting_mobject, anim.ending_mobject) + anim.starting_mobject.align_data(anim.ending_mobject) Transform.__init__(self, start_anim.mobject, end_anim.mobject, **kwargs) #Rewire starting and ending mobjects diff --git a/camera.py b/camera.py index 3770a249..6c66a1e3 100644 --- a/camera.py +++ b/camera.py @@ -71,19 +71,18 @@ class Camera(object): mob.nonempty_family_members() for mob in mobjects ]) - vect_mobjects = [] for mobject in mobjects: if isinstance(mobject, VectorizedMobject): - vect_mobjects.append(mobject) + self.display_vectorized(mobject) elif isinstance(mobject, PointCloudMobject): self.display_point_cloud( mobject.points, mobject.rgbs, - self.adjusted_thickness(mobject.point_thickness) + self.adjusted_thickness(mobject.stroke_width) ) else: - raise Exception("I don't know how to display that") - if vect_mobjects: - self.display_vectorized(vect_mobjects) + #TODO + print mobject + # raise Exception("I don't know how to display that") # def display_region(self, region): # (h, w) = self.pixel_shape @@ -98,25 +97,23 @@ class Camera(object): # self.pixel_array[covered] = rgb - def display_vectorized(self, vect_mobjects): + def display_vectorized(self, vect_mobject): im = Image.fromarray(self.pixel_array, mode = "RGB") canvas = aggdraw.Draw(im) - for mob in vect_mobjects: - pen, fill = self.get_pen_and_fill(mob) - #TODO, fill - pathstring = self.get_pathstring( - self.points_to_pixel_coords(mob.points), - closed = mob.is_closed() - ) - symbol = aggdraw.Symbol(pathstring) - canvas.symbol((0, 0), symbol, pen, fill) + 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() + ) + symbol = aggdraw.Symbol(pathstring) + canvas.symbol((0, 0), symbol, pen, fill) canvas.flush() - self.pixel_array = np.array(im) + self.pixel_array[:,:] = np.array(im) def get_pen_and_fill(self, vect_mobject): pen = aggdraw.Pen( - vect_mobject.get_color().get_web(), - vect_mobject.point_thickness + vect_mobject.get_stroke_color().get_web(), + vect_mobject.stroke_width ) fill = aggdraw.Brush( vect_mobject.get_fill_color().get_web(), @@ -124,8 +121,6 @@ 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 diff --git a/helpers.py b/helpers.py index 74a909fd..7cdeb790 100644 --- a/helpers.py +++ b/helpers.py @@ -25,6 +25,23 @@ def compass_directions(n = 4, start_vect = UP): for k in range(n) ] +def diag_to_matrix(l_and_u, diag): + """ + Converts array whose rows represent diagonal + entries of a matrix into the matrix itself. + See scipy.linalg.solve_banded + """ + l, u = l_and_u + dim = diag.shape[1] + matrix = np.zeros((dim, dim)) + for i in range(l+u+1): + np.fill_diagonal( + matrix[max(0,i-u):,max(0,u-i):], + diag[i,max(0,u-i):] + ) + return matrix + + def bezier(points): n = len(points) - 1 return lambda t : sum([ diff --git a/mobject/image_mobject.py b/mobject/image_mobject.py index 4641471c..485c90df 100644 --- a/mobject/image_mobject.py +++ b/mobject/image_mobject.py @@ -15,7 +15,7 @@ class ImageMobject(Mobject): "filter_color" : "black", "invert" : True, "use_cache" : True, - "point_thickness" : 1, + "stroke_width" : 1, "scale_value" : 1.0, "should_center" : True, } diff --git a/mobject/mobject.py b/mobject/mobject.py index 5d9c5445..b6e43c73 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -18,7 +18,7 @@ class Mobject(object): #Number of numbers used to describe a point (3 for pos, 3 for normal vector) CONFIG = { "color" : WHITE, - "point_thickness" : DEFAULT_POINT_THICKNESS, + "stroke_width" : DEFAULT_POINT_THICKNESS, "name" : None, "display_mode" : "points", #TODO, REMOVE "dim" : 3, @@ -109,7 +109,7 @@ class Mobject(object): def rotate(self, angle, axis = OUT, axes = []): if len(axes) == 0: axes = [axis] - rot_matrix = np.identity(self.DIM) + rot_matrix = np.identity(self.dim) for axis in axes: rot_matrix = np.dot(rot_matrix, rotation_matrix(angle, axis)) t_rot_matrix = np.transpose(rot_matrix) @@ -135,7 +135,7 @@ class Mobject(object): alphas = alphas**wag_factor mob.points += np.dot( alphas.reshape((len(alphas), 1)), - np.array(direction).reshape((1, mob.DIM)) + np.array(direction).reshape((1, mob.dim)) ) return self @@ -310,7 +310,7 @@ class Mobject(object): return 0 def get_merged_array(self, array_attr): - result = np.zeros((0, self.DIM)) + result = np.zeros((0, self.dim)) for mob in self.nonempty_family_members(): result = np.append(result, getattr(mob, array_attr), 0) return result @@ -327,7 +327,7 @@ class Mobject(object): return len(self.points) def get_critical_point(self, direction): - result = np.zeros(self.DIM) + result = np.zeros(self.dim) for dim in [0, 1]: if direction[dim] <= 0: min_point = self.reduce_across_dimension(np.min, np.min, dim) @@ -350,7 +350,7 @@ class Mobject(object): return self.get_critical_point(direction) def get_center(self): - return self.get_critical_point(np.zeros(self.DIM)) + return self.get_critical_point(np.zeros(self.dim)) def get_center_of_mass(self): return np.apply_along_axis(np.mean, 0, self.get_all_points()) @@ -404,52 +404,52 @@ class Mobject(object): self.submobject_family() ) - ## Alignment - - @staticmethod - def align_data(mobject1, mobject2): - count1 = len(mobject1.points) - count2 = len(mobject2.points) - if count1 != count2: - if count1 < count2: - smaller = mobject1 - target_size = count2 - else: - smaller = mobject2 - target_size = count1 - if len(smaller.points) == 0: - smaller.add_points( - [np.zeros(smaller.DIM)], - color = BLACK - ) - smaller.apply_over_attr_arrays( - lambda a : streth_array_to_length(a, target_size) - ) + ## Alignment + def align_data(self, mobject): + self.align_points(mobject) #Recurse - diff = len(mobject1.sub_mobjects) - len(mobject2.sub_mobjects) - - if diff < 0: - larger, smaller = mobject2, mobject1 - elif diff > 0: - larger, smaller = mobject1, mobject2 - if diff != 0: + diff = len(self.sub_mobjects) - len(mobject.sub_mobjects) + if diff != 0: + if diff < 0: + larger, smaller = mobject, self + elif diff > 0: + larger, smaller = self, mobject for sub_mob in larger.sub_mobjects[-abs(diff):]: - smaller.add(Point(sub_mob.get_center())) - for m1, m2 in zip(mobject1.sub_mobjects, mobject2.sub_mobjects): - Mobject.align_data(m1, m2) + smaller.add(sub_mob.get_point_mobject()) + for m1, m2 in zip(self.sub_mobjects, mobject.sub_mobjects): + m1.align_data(m2) - def interpolate(self, mobject1, mobject2, alpha): + def get_point_mobject(self): + """ + The simplest mobject to be transformed to or from self. + Should by a point of the appropriate type + """ + raise Exception("Not implemented") + + def align_points(self, mobject): + count1 = self.get_num_points() + count2 = mobject.get_num_points() + if count1 < count2: + self.align_points_with_larger(mobject) + elif count2 < count1: + mobject.align_points_with_larger(self) + return self + + def align_points_with_larger(self, larger_mobject): + raise Exception("Not implemented") + + def interpolate(self, mobject1, mobject2, alpha, path_func): """ Turns target_mobject into an interpolation between mobject1 and mobject2. """ - #TODO - Mobject.align_data(mobject1, mobject2) - for attr in self.get_array_attrs(): - setattr(self, attr, interpolate( - getattr(mobject1, attr), - getattr(mobject2, attr), - alpha)) + self.points = path_func( + mobject1.points, mobject2.points, alpha + ) + self.interpolate_color(mobject1, mobject2, alpha) + + def interpolate_color(self, mobject1, mobject2, alpha): + raise Exception("Not implemented") diff --git a/mobject/point_cloud_mobject.py b/mobject/point_cloud_mobject.py index 93c026f9..5d45ccf3 100644 --- a/mobject/point_cloud_mobject.py +++ b/mobject/point_cloud_mobject.py @@ -110,6 +110,23 @@ class PointCloudMobject(Mobject): index = alpha*(self.get_num_points()-1) return self.points[index] + # Alignment + def align_points_with_larger(self, larger_mobject): + assert(isinstance(larger_mobject, PointCloudMobject)) + self.apply_over_attr_arrays( + lambda a : streth_array_to_length( + a, larger_mobject.get_num_points() + ) + ) + + def get_point_mobject(self): + return Point(self.get_center()) + + def interpolate_color(self, mobject1, mobject2, alpha): + self.rgbs = interpolate( + mobject1.rgbs, mobject2.rgbs, alpha + ) + #TODO, Make the two implementations bellow non-redundant class Mobject1D(PointCloudMobject): @@ -146,13 +163,11 @@ class Mobject2D(PointCloudMobject): -class Point(Mobject): +class Point(PointCloudMobject): CONFIG = { "color" : BLACK, } def __init__(self, location = ORIGIN, **kwargs): - digest_locals(self) - Mobject.__init__(self, **kwargs) + PointCloudMobject.__init__(self, **kwargs) + self.add_points([location]) - def generate_points(self): - self.add_points([self.location]) diff --git a/mobject/tex_mobject.py b/mobject/tex_mobject.py index 6c6106c6..473594d6 100644 --- a/mobject/tex_mobject.py +++ b/mobject/tex_mobject.py @@ -8,7 +8,7 @@ class TexMobject(Mobject): CONFIG = { "template_tex_file" : TEMPLATE_TEX_FILE, "color" : WHITE, - "point_thickness" : 1, + "stroke_width" : 1, "should_center" : True, } def __init__(self, expression, **kwargs): diff --git a/mobject/vectorized_mobject.py b/mobject/vectorized_mobject.py index 4410da8a..172580a0 100644 --- a/mobject/vectorized_mobject.py +++ b/mobject/vectorized_mobject.py @@ -35,7 +35,7 @@ class VectorizedMobject(Mobject): def get_fill_opacity(self): return self.fill_opacity - def get_storke_color(self): + def get_stroke_color(self): return Color(rgb = self.stroke_rgb) #TODO, get color? Specify if stroke or fill @@ -88,23 +88,23 @@ 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) return self def set_points_smoothly(self, points): - if self.is_closed(): - points = np.append( - points, - [points[0], points[1]], - axis = 0 - ) + if len(points) <= 1: + return self + points = self.close_if_needed(points) num_handles = len(points) - 1 #Must solve 2*num_handles equations to get the handles. #l and u are the number of lower an upper diagonal rows #in the matrix to solve. - l, u = 2, 1 + l, u = 2, 1 #diag is a representation of the matrix in diagonal form #See https://www.particleincell.com/2012/bezier-splines/ #for how to arive at these equations @@ -115,6 +115,8 @@ class VectorizedMobject(Mobject): diag[1,1::2] = 1 diag[2,1:-2:2] = -2 diag[3,0:-3:2] = 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 @@ -122,25 +124,36 @@ class VectorizedMobject(Mobject): b[1::2] = 2*points[1:] b[0] = points[0] b[-1] = points[-1] - + solve_func = lambda b : linalg.solve_banded( + (l, u), diag, b + ) + if self.is_closed(): + #Get equations to relate first and last points + matrix = diag_to_matrix((l, u), diag) + #last row handles second derivative + matrix[-1, [0, 1]] = matrix[0, [0, 1]] + #first row handles first derivative + matrix[0,:] = np.zeros(matrix.shape[1]) + matrix[0,[0, -1]] = [1, 1] + b[0] = 2*points[0] + b[-1] = np.zeros(self.dim) + solve_func = lambda b : linalg.solve(matrix, b) handle_pairs = np.zeros((2*num_handles, self.dim)) for i in range(self.dim): - handle_pairs[:,i] = linalg.solve_banded( - (l, u), diag, b[:,i] - ) + handle_pairs[:,i] = solve_func(b[:,i]) handles1 = handle_pairs[0::2] handles2 = handle_pairs[1::2] - if self.is_closed(): - #Ignore last point that was artificially added - #to smooth out the closing. - #TODO, is the the best say to handle this? - handles1[0] = handles1[-1] - points = points[:-1] - handles1 = handles1[:-1] - handles2 = handles2[:-1] - self.set_anchors_and_handles(points, handles1, handles2) + 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, mode = "smooth"): points = np.array(points) @@ -157,17 +170,63 @@ class VectorizedMobject(Mobject): ## Information about line def get_num_points(self): - pass + return (len(self.points) - 1)/3 + 1 def point_from_proportion(self, alpha): - pass - - - - - - - + num_cubics = self.get_num_points()-1 + interpoint_alpha = num_cubics*(alpha % (1./num_cubics)) + index = 3*int(alpha*num_cubics) + cubic = bezier(self.points[index:index+4]) + return cubic(interpoint_alpha) + + + ## 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 get_point_mobject(self): + return VectorizedPoint(self.get_center()) + + def interpolate_color(self, mobject1, mobject2, alpha): + attrs = [ + "stroke_rgb", + "stroke_width", + "fill_rgb", + "fill_opacity", + ] + for attr in attrs: + setattr(self, attr, interpolate( + getattr(mobject1, attr), + getattr(mobject2, attr), + alpha + )) + + + + +class VectorizedPoint(VectorizedMobject): + CONFIG = { + "color" : BLACK, + } + def __init__(self, location = ORIGIN, **kwargs): + VectorizedMobject.__init__(self, **kwargs) + self.set_points([location]) diff --git a/old_projects/complex_multiplication_article.py b/old_projects/complex_multiplication_article.py index 9af406fa..0b9f9fad 100644 --- a/old_projects/complex_multiplication_article.py +++ b/old_projects/complex_multiplication_article.py @@ -14,7 +14,7 @@ from scene import Scene from topics.complex_numbers import * DEFAULT_PLANE_CONFIG = { - "point_thickness" : 2*DEFAULT_POINT_THICKNESS + "stroke_width" : 2*DEFAULT_POINT_THICKNESS class SuccessiveComplexMultiplications(ComplexMultiplication): @@ -144,7 +144,7 @@ class DrawSolutionsToZToTheNEqualsW(Scene): plane = ComplexPlane(**plane_config) circle = Circle( radius = radius*zoom_value, - point_thickness = plane.point_thickness + stroke_width = plane.stroke_width ) solutions = [ radius*np.exp(complex(0, 1)*(2*np.pi*k + theta)/n) @@ -222,7 +222,7 @@ class DrawComplexAngleAndMagnitude(Scene): Line( start, end, color = color, - point_thickness = self.plane.point_thickness + stroke_width = self.plane.stroke_width ) for start, end, color in zip( [ORIGIN, point[0]*RIGHT, ORIGIN], diff --git a/old_projects/matrix_as_transform_2d.py b/old_projects/matrix_as_transform_2d.py index c9049bf3..18edb899 100644 --- a/old_projects/matrix_as_transform_2d.py +++ b/old_projects/matrix_as_transform_2d.py @@ -10,7 +10,7 @@ from helpers import * from scene import Scene from number_line import NumberLineScene -ARROW_CONFIG = {"point_thickness" : 2*DEFAULT_POINT_THICKNESS} +ARROW_CONFIG = {"stroke_width" : 2*DEFAULT_POINT_THICKNESS} LIGHT_RED = RED_E def matrix_to_string(matrix): @@ -55,7 +55,7 @@ class ShowMultiplication(NumberLineScene): def construct(self, num, show_original_line): config = { "density" : max(abs(num), 1)*DEFAULT_POINT_DENSITY_1D, - "point_thickness" : 2*DEFAULT_POINT_THICKNESS + "stroke_width" : 2*DEFAULT_POINT_THICKNESS } if abs(num) < 1: config["numerical_radius"] = SPACE_WIDTH/num @@ -114,7 +114,7 @@ class ExamplesOfNonlinearOneDimensionalTransforms(NumberLineScene): self.clear() self.add(self.nonlinear) config = { - "point_thickness" : 2*DEFAULT_POINT_THICKNESS, + "stroke_width" : 2*DEFAULT_POINT_THICKNESS, "density" : 5*DEFAULT_POINT_DENSITY_1D, } NumberLineScene.construct(self, **config) @@ -143,7 +143,7 @@ class ShowTwoThenThree(ShowMultiplication): def construct(self): config = { - "point_thickness" : 2*DEFAULT_POINT_THICKNESS, + "stroke_width" : 2*DEFAULT_POINT_THICKNESS, "density" : 6*DEFAULT_POINT_DENSITY_1D, } NumberLineScene.construct(self, **config) @@ -162,7 +162,7 @@ class TransformScene2D(Scene): "x_radius" : 2*SPACE_WIDTH, "y_radius" : 2*SPACE_WIDTH, "density" : DEFAULT_POINT_DENSITY_1D*density_factor, - "point_thickness" : 2*DEFAULT_POINT_THICKNESS + "stroke_width" : 2*DEFAULT_POINT_THICKNESS } if not use_faded_lines: config["x_faded_line_frequency"] = None @@ -318,7 +318,7 @@ class ExamplesOfNonlinearTwoDimensionalTransformations(Scene): "x_radius" : 2*SPACE_WIDTH, "y_radius" : 2*SPACE_WIDTH, "density" : 3*DEFAULT_POINT_DENSITY_1D, - "point_thickness" : 2*DEFAULT_POINT_THICKNESS + "stroke_width" : 2*DEFAULT_POINT_THICKNESS } number_plane = NumberPlane(**config) numbers = number_plane.get_coordinate_labels() @@ -372,7 +372,7 @@ class TrickyExamplesOfNonlinearTwoDimensionalTransformations(Scene): "x_radius" : 1.2*SPACE_WIDTH, "y_radius" : 1.2*SPACE_WIDTH, "density" : 10*DEFAULT_POINT_DENSITY_1D, - "point_thickness" : 2*DEFAULT_POINT_THICKNESS + "stroke_width" : 2*DEFAULT_POINT_THICKNESS } number_plane = NumberPlane(**config) phrase1, phrase2 = TextMobject([ diff --git a/old_projects/music_and_measure.py b/old_projects/music_and_measure.py index 546142f4..989abaec 100644 --- a/old_projects/music_and_measure.py +++ b/old_projects/music_and_measure.py @@ -95,7 +95,7 @@ class OpenInterval(Mobject): class Piano(ImageMobject): CONFIG = { - "point_thickness" : 1, + "stroke_width" : 1, "invert" : False, "scale_value" : 0.5 } @@ -113,7 +113,7 @@ class Piano(ImageMobject): for count in range(14): key = Mobject( color = "white", - point_thickness = 1 + stroke_width = 1 ) x0 = left + count*self.ivory_jump x1 = x0 + self.ivory_jump @@ -664,7 +664,7 @@ class ConstructPiano(Scene): askew = deepcopy(keys[-1]) keys[-1].rotate_in_place(np.pi/5) for key in keys: - key.point_thickness = 1 + key.stroke_width = 1 key_copy = deepcopy(key).to_corner(DOWN+LEFT) key_copy.scale_in_place(0.25) key_copy.shift(1.8*random.random()*SPACE_WIDTH*RIGHT) diff --git a/topics/three_dimensions.py b/topics/three_dimensions.py index ecb32275..e9e14f92 100644 --- a/topics/three_dimensions.py +++ b/topics/three_dimensions.py @@ -8,7 +8,7 @@ from helpers import * class Stars(Mobject): CONFIG = { - "point_thickness" : 1, + "stroke_width" : 1, "radius" : SPACE_WIDTH, "num_points" : 1000, }