diff --git a/animation/transform.py b/animation/transform.py index 2b7ca1e5..a47998cb 100644 --- a/animation/transform.py +++ b/animation/transform.py @@ -30,6 +30,15 @@ def clockwise_path(start_points, end_points, alpha): def counterclockwise_path(start_points, end_points, alpha): return semi_circular_path(start_points, end_points, alpha, OUT) +def get_best_interpolation_function(angle): + angle = (angle + np.pi)%(2*np.pi) - np.pi + if abs(angle) < np.pi/2: + return straight_path + elif angle > 0: + return counterclockwise_path + else: + return clockwise_path + class Transform(Animation): DEFAULT_CONFIG = { "run_time" : DEFAULT_TRANSFORM_RUN_TIME, diff --git a/constants.py b/constants.py index 7aeccdcf..2749d1c8 100644 --- a/constants.py +++ b/constants.py @@ -60,7 +60,7 @@ TEX_IMAGE_DIR = os.path.join(IMAGE_DIR, "Tex") MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects") IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image") -for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TEX_DIR, TMP_IMAGE_DIR, +for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TEX_DIR, TEX_IMAGE_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR]: if not os.path.exists(folder): os.mkdir(folder) diff --git a/displayer.py b/displayer.py index cad94176..17b893b6 100644 --- a/displayer.py +++ b/displayer.py @@ -49,49 +49,58 @@ def paint_mobjects(mobjects, image_array = None): for mobject in mobjects: if mobject.get_num_points() == 0: continue - points, rgbs = place_on_screen(mobject.points, mobject.rgbs, - space_width, space_height) + points_and_rgbs = np.append( + mobject.points, + 255*mobject.rgbs, + axis = 1 + ) + points_and_rgbs = place_on_screen( + points_and_rgbs, + space_width, space_height + ) #Map points to pixel space, which requires rescaling and shifting #Remember, 2*space_height -> height - points[:,0] = points[:,0]*width/space_width/2 + width/2 + points_and_rgbs[:,0] = points_and_rgbs[:,0]*width/space_width/2 + width/2 #Flip on y-axis as you go - points[:,1] = -1*points[:,1]*height/space_height/2 + height/2 - points, rgbs = add_thickness( - points.astype('int'), rgbs, + points_and_rgbs[:,1] = -1*points_and_rgbs[:,1]*height/space_height/2 + height/2 + points_and_rgbs = add_thickness( + points_and_rgbs.astype('int'), mobject.point_thickness, width, height ) + points, rgbs = points_and_rgbs[:,:2], points_and_rgbs[:,2:] flattener = np.array([[1], [width]], dtype = 'int') indices = np.dot(points, flattener)[:,0] - pixels[indices] = (255*rgbs).astype('uint8') + pixels[indices] = rgbs.astype('uint8') pixels = pixels.reshape((height, width, 3)).astype('uint8') return pixels -def add_thickness(pixel_indices, rgbs, thickness, width, height): +def add_thickness(pixel_indices_and_rgbs, thickness, width, height): """ Imagine dragging each pixel around like a paintbrush in - a plus-sign-shaped pixel arrangement surrounding it + a plus-sign-shaped pixel arrangement surrounding it. + + Pass rgb = None to do nothing to them """ thickness = adjusted_thickness(thickness, width, height) - original = np.array(pixel_indices) - original_rgbs = np.array(rgbs) + original = np.array(pixel_indices_and_rgbs) + n_extra_columns = pixel_indices_and_rgbs.shape[1] - 2 for nudge in range(-thickness/2+1, thickness/2+1): if nudge == 0: continue for x, y in [[nudge, 0], [0, nudge]]: - pixel_indices = np.append( - pixel_indices, - original+[x, y], + pixel_indices_and_rgbs = np.append( + pixel_indices_and_rgbs, + original+([x, y] + [0]*n_extra_columns), axis = 0 ) - rgbs = np.append(rgbs, original_rgbs, axis = 0) - admissibles = (pixel_indices[:,0] >= 0) & \ - (pixel_indices[:,0] < width) & \ - (pixel_indices[:,1] >= 0) & \ - (pixel_indices[:,1] < height) - return pixel_indices[admissibles], rgbs[admissibles] + admissibles = (pixel_indices_and_rgbs[:,0] >= 0) & \ + (pixel_indices_and_rgbs[:,0] < width) & \ + (pixel_indices_and_rgbs[:,1] >= 0) & \ + (pixel_indices_and_rgbs[:,1] < height) + return pixel_indices_and_rgbs[admissibles] def adjusted_thickness(thickness, width, height): big_width = PRODUCTION_QUALITY_DISPLAY_CONFIG["width"] @@ -99,38 +108,50 @@ def adjusted_thickness(thickness, width, height): factor = (big_width + big_height) / (width + height) return 1 + (thickness-1)/factor -def place_on_screen(points, rgbs, space_width, space_height): +def place_on_screen(points_and_rgbs, space_width, space_height): """ Projects points to 2d space and remove those outside a - the space constraints + the space constraints. + + Pass rbgs = None to do nothing to them. """ - # camera_distance = 10 - points = np.array(points[:, :2]) - # for i in range(2): - # points[:,i] *= camera_distance/(camera_distance-mobject.points[:,2]) - rgbs = np.array(rgbs) + # Remove 3rd column + points_and_rgbs = np.append( + points_and_rgbs[:, :2], + points_and_rgbs[:, 3:], + axis = 1 + ) #Removes points out of space - to_keep = (abs(points[:,0]) < space_width) & \ - (abs(points[:,1]) < space_height) - return points[to_keep], rgbs[to_keep] + to_keep = (abs(points_and_rgbs[:,0]) < space_width) & \ + (abs(points_and_rgbs[:,1]) < space_height) + return points_and_rgbs[to_keep] + +def get_file_path(name, extension): + file_path = os.path.join(MOVIE_DIR, name) + if not file_path.endswith(".mp4"): + file_path += ".mp4" + directory = os.path.split(file_path)[0] + if not os.path.exists(directory): + os.makedirs(directory) + return file_path def write_to_gif(scene, name): #TODO, find better means of compression if not name.endswith(".gif"): name += ".gif" - filepath = os.path.join(GIF_DIR, name) + file_path = os.path.join(GIF_DIR, name) temppath = os.path.join(GIF_DIR, "Temp.gif") print "Writing " + name + "..." images = [Image.fromarray(frame) for frame in scene.frames] writeGif(temppath, images, scene.frame_duration) print "Compressing..." - os.system("gifsicle -O " + temppath + " > " + filepath) + os.system("gifsicle -O " + temppath + " > " + file_path) os.system("rm " + temppath) def write_to_movie(scene, name): - filepath = os.path.join(MOVIE_DIR, name) + ".mp4" - print "Writing to %s"%filepath + file_path = get_file_path(name, ".mp4") + print "Writing to %s"%file_path fps = int(1/scene.display_config["frame_duration"]) dim = (scene.display_config["width"], scene.display_config["height"]) @@ -149,12 +170,11 @@ def write_to_movie(scene, name): '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-loglevel', 'error', - filepath, + file_path, ] process = sp.Popen(command, stdin=sp.PIPE) for frame in scene.frames: process.stdin.write(frame.tostring()) - process.stdin.close() process.wait() diff --git a/mobject/function_graphs.py b/mobject/function_graphs.py index cac7a577..ba768f63 100644 --- a/mobject/function_graphs.py +++ b/mobject/function_graphs.py @@ -2,6 +2,7 @@ import numpy as np import itertools as it from mobject import Mobject, Mobject1D, Mobject2D, CompoundMobject +from simple_mobjects import Arrow, Line from image_mobject import tex_mobject from constants import * from helpers import * @@ -255,9 +256,65 @@ class NumberPlane(Mobject1D): self.add(*self.get_coordinate_labels(x_vals, y_vals)) return self + def get_vector(self, coords, **kwargs): + if len(coords) == 2: + coords = tuple(list(coords) + [0]) + arrow = Arrow(ORIGIN, coords, **kwargs) + arrow.remove_tip() + arrow.align_data(Line(ORIGIN, SPACE_WIDTH*LEFT)) + arrow.add_tip() + return arrow + class ComplexPlane(NumberPlane): - #TODO - pass + DEFAULT_CONFIG = { + "color" : "lightgreen", + "unit_to_spatial_width" : 1, + "line_frequency" : 1, + "faded_line_frequency" : 0.5, + "number_at_center" : complex(0), + } + def __init__(self, **kwargs): + digest_config(self, ComplexPlane, kwargs) + kwargs.update({ + "x_unit_to_spatial_width" : self.unit_to_spatial_width, + "y_uint_to_spatial_height" : self.unit_to_spatial_width, + "x_line_frequency" : self.line_frequency, + "x_faded_line_frequency" : self.faded_line_frequency, + "y_line_frequency" : self.line_frequency, + "y_faded_line_frequency" : self.faded_line_frequency, + "num_pair_at_center" : (self.number_at_center.real, self.number_at_center.imag), + }) + NumberPlane.__init__(self, **kwargs) + + def number_to_point(self, number): + number = complex(number) + return self.num_pair_to_point((number.real, number.imag)) + + def get_coordinate_labels(self, *numbers): + result = [] + nudge = 0.1*(DOWN+RIGHT) + if len(numbers) == 0: + numbers = range(-int(self.x_radius), int(self.x_radius)) + numbers += [ + complex(0, y) + for y in range(-int(self.y_radius), int(self.y_radius)) + ] + for number in numbers: + point = self.number_to_point(number) + if number == 0: + num_str = "0" + else: + num_str = str(number).replace("j", "i") + num = tex_mobject(num_str) + num.scale(self.number_scale_factor) + num.shift(point-num.get_corner(UP+LEFT)+nudge) + result.append(num) + return result + + def add_coordinates(self, *numbers): + self.add(*self.get_coordinate_labels(*numbers)) + return self + diff --git a/mobject/mobject.py b/mobject/mobject.py index f836752a..48506126 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -321,18 +321,17 @@ class Mobject(object): #Typically implemented in subclass, unless purposefully left blank pass - ### Static Methods ### - def align_data(mobject1, mobject2): - count1, count2 = mobject1.get_num_points(), mobject2.get_num_points() + def align_data(self, mobject): + count1, count2 = self.get_num_points(), mobject.get_num_points() if count1 == 0: - mobject1.add_points([(0, 0, 0)]) + self.add_points([(0, 0, 0)]) if count2 == 0: - mobject2.add_points([(0, 0, 0)]) + mobject.add_points([(0, 0, 0)]) if count1 == count2: return for attr in ['points', 'rgbs']: - new_arrays = make_even(getattr(mobject1, attr), getattr(mobject2, attr)) - for array, mobject in zip(new_arrays, [mobject1, mobject2]): + new_arrays = make_even(getattr(self, attr), getattr(mobject, attr)) + for array, mobject in zip(new_arrays, [self, mobject]): setattr(mobject, attr, np.array(array)) def interpolate(mobject1, mobject2, target_mobject, alpha): @@ -342,9 +341,10 @@ class Mobject(object): """ Mobject.align_data(mobject1, mobject2) for attr in ['points', 'rgbs']: - new_array = (1 - alpha) * getattr(mobject1, attr) + \ - alpha * getattr(mobject2, attr) - setattr(target_mobject, attr, new_array) + setattr(target_mobject, attr, interpolate( + getattr(mobject1, attr), + getattr(mobject2, attr), + alpha)) #TODO, Make the two implementations bellow not redundant class Mobject1D(Mobject): diff --git a/sample_script.py b/sample_script.py index 8abb4098..8452ed39 100644 --- a/sample_script.py +++ b/sample_script.py @@ -16,9 +16,10 @@ from script_wrapper import command_line_create_scene class SampleScene(Scene): def construct(self): - circle = Circle().repeat(6) - self.play(Transform(circle, Square(), run_time = 3)) - self.dither() + c = ComplexPlane().add_coordinates() + + self.add_local_mobjects() + if __name__ == "__main__": diff --git a/scene/scene.py b/scene/scene.py index 31de65a3..d0d2da74 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -51,7 +51,7 @@ class Scene(object): return self.name return self.__class__.__name__ + \ self.args_to_string(*self.construct_args) - + def set_name(self, name): self.name = name return self @@ -62,6 +62,8 @@ class Scene(object): in the order with which they are entered. """ for mobject in mobjects: + if not isinstance(mobject, Mobject): + raise Exception("Adding something which is not a mobject") #In case it's already in there, it should #now be closer to the foreground. self.remove(mobject) @@ -86,6 +88,15 @@ class Scene(object): self.mobjects.remove(mobject) return self + def bring_to_front(self, mobject): + self.add(mobject) + return self + + def bring_to_back(self, mobject): + self.remove(mobject) + self.mobjects = [mobject] + self.mobjects + return self + def clear(self): self.reset_background() self.remove(*self.mobjects) @@ -174,11 +185,8 @@ class Scene(object): animation.clean_up() return self - def apply(self, mob_to_anim_func, **kwargs): - self.play(*[ - mob_to_anim_func(mobject) - for mobject in self.mobjects - ]) + def apply(self, mobject_method, *args, **kwargs): + self.play(ApplyMethod(mobject_method, *args, **kwargs)) def get_frame(self): return disp.paint_mobjects(self.mobjects, self.background) diff --git a/scene/tk_scene.py b/scene/tk_scene.py index 63939543..dfa05b72 100644 --- a/scene/tk_scene.py +++ b/scene/tk_scene.py @@ -7,7 +7,7 @@ import time class TkSceneRoot(Tkinter.Tk): def __init__(self, scene): if scene.frames == []: - raise str(scene) + " has no frames!" + raise Exception(str(scene) + " has no frames!") Tkinter.Tk.__init__(self) height, width = scene.shape diff --git a/script_wrapper.py b/script_wrapper.py index a9d1f017..55a63f40 100644 --- a/script_wrapper.py +++ b/script_wrapper.py @@ -66,7 +66,7 @@ def get_configuration(sys_argv): if len(args) > 0: config["scene_name"] = args[0] if len(args) > 1: - config["args_extension"] = args[1] + config["args_extension"] = " ".join(args[1:]) return config def handle_scene(scene, **config): @@ -128,13 +128,13 @@ def command_line_create_scene(movie_prefix = ""): "announce_construction" : True } for SceneClass in scene_classes: - args_list = SceneClass.args_list or [()] + args_list = SceneClass.args_list preset_extensions = [ SceneClass.args_to_string(*args) for args in args_list ] if config["write_all"]: - args_to_run = args_list + args_to_run = args_list or [()] elif config["args_extension"] in preset_extensions: index = preset_extensions.index(config["args_extension"]) args_to_run = [args_list[index]] diff --git a/scripts/complex_actions.py b/scripts/complex_actions.py new file mode 100644 index 00000000..e2222f53 --- /dev/null +++ b/scripts/complex_actions.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python + +import numpy as np +import itertools as it +from copy import deepcopy +import sys + + +from animation import * +from mobject import * +from constants import * +from region import * +from scene import Scene +from script_wrapper import command_line_create_scene + +MOVIE_PREFIX = "complex_actions/" + +class ComplexMultiplication(Scene): + @staticmethod + def args_to_string(multiplier): + return filter(lambda c : c not in "()", str(multiplier)) + + @staticmethod + def string_to_args(num_string): + return complex(num_string) + + def construct(self, multiplier): + norm = np.linalg.norm(multiplier) + arg = np.log(multiplier).imag + plane_config = { + "faded_line_frequency" : 0 + } + if norm > 1: + plane_config["density"] = norm*DEFAULT_POINT_DENSITY_1D + radius = SPACE_WIDTH + if norm > 0 and norm < 1: + radius /= norm + plane_config["x_radius"] = plane_config["y_radius"] = radius + plane = ComplexPlane(**plane_config) + self.anim_config = { + "run_time" : 2.0, + "interpolation_function" : get_best_interpolation_function(arg) + } + + background = ComplexPlane(color = "grey") + labels = background.get_coordinate_labels() + self.paint_into_background(background, *labels) + arrow, new_arrow = [ + plane.get_vector(plane.number_to_point(z), color = "skyblue") + for z in [1, multiplier] + ] + self.add(arrow) + self.additional_animations = [Transform( + arrow, new_arrow, **self.anim_config + )] + + self.mobjects_to_multiply = [plane] + self.mobjects_to_move_without_molding = [] + self.multiplier = multiplier + self.plane = plane + if self.__class__ == ComplexMultiplication: + self.apply_multiplication() + + def apply_multiplication(self): + def func((x, y, z)): + complex_num = self.multiplier*complex(x, y) + return (complex_num.real, complex_num.imag, z) + mobjects = self.mobjects_to_multiply+self.mobjects_to_move_without_molding + mobjects += [anim.mobject for anim in self.additional_animations] + self.add(*mobjects) + full_multiplications = [ + ApplyMethod(mobject.apply_function, func, **self.anim_config) + for mobject in self.mobjects_to_multiply + ] + movements_with_plane = [ + ApplyMethod( + mobject.shift, + func(mobject.get_center())-mobject.get_center(), + **self.anim_config + ) + for mobject in self.mobjects_to_move_without_molding + ] + self.dither() + self.play(*reduce(op.add, [ + full_multiplications, + movements_with_plane, + self.additional_animations + ])) + self.dither() + +class MultiplicationWithDot(ComplexMultiplication): + @staticmethod + def args_to_string(multiplier, dot_coords): + start = ComplexMultiplication.args_to_string(multiplier) + return start + "WithDotAt%d-%d"%dot_coords[:2] + + @staticmethod + def string_to_args(arg_string): + parts = arg_string.split() + if len(parts) < 2 or len(parts) > 3: + raise Exception("Invalid arguments") + multiplier = complex(parts[0]) + tup_string = filter(lambda c : c not in "()", parts[1]) + nums = tuple(map(int, tup_string.split(",")))[:2] + return multiplier, nums + + def construct(self, multiplier, dot_coords): + ComplexMultiplication.construct(self, multiplier) + self.mobjects_to_move_without_molding.append( + Dot().shift(dot_coords) + ) + self.apply_multiplication() + + +class ShowComplexPower(ComplexMultiplication): + @staticmethod + def args_to_string(multiplier, num_repeats): + start = ComplexMultiplication.args_to_string(multiplier) + return start + "ToThe%d"%num_repeats + + @staticmethod + def string_to_args(arg_string): + parts = arg_string.split() + if len(parts) < 2 or len(parts) > 3: + raise Exception("Invalid arguments") + multiplier = complex(parts[0]) + num_repeats = int(parts[1]) + return multiplier, num_repeats + + def construct(self, multiplier, num_repeats): + ComplexMultiplication.construct(self, multiplier) + for x in range(num_repeats): + arrow_transform = Transform(*[ + self.plane.get_vector(point, color = "skyblue") + for z in [multiplier**(x), multiplier**(x+1)] + for point in [self.plane.number_to_point(z)] + ], **self.anim_config) + self.remove(*[anim.mobject for anim in self.additional_animations]) + self.additional_animations = [arrow_transform] + self.apply_multiplication() + + + + + + + + + + + + + + + +if __name__ == "__main__": + command_line_create_scene(MOVIE_PREFIX) \ No newline at end of file diff --git a/scripts/matrix_as_transform_2d.py b/scripts/matrix_as_transform_2d.py index 30607d1b..bb78e7d3 100644 --- a/scripts/matrix_as_transform_2d.py +++ b/scripts/matrix_as_transform_2d.py @@ -224,7 +224,6 @@ class ShowMatrixTransform(TransformScene2D): self.add_x_y_arrows() else: self.add_number_plane(**number_plane_config) - self.save_image() if show_matrix: self.add(matrix_mobject(matrix).to_corner(UP+LEFT)) def func(mobject):