From 948e1e3038bda3f4c01e33705c8598c25b76ebc2 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 22 Mar 2015 13:33:02 -0600 Subject: [PATCH] post epii project state, without tex files --- animate.py | 478 ++++++++++++++ constants.py | 36 ++ displayer.py | 124 ++++ epii/__init__.py | 0 epii/cccd_animations.py | 787 +++++++++++++++++++++++ epii/dexp_generate_images.py | 16 + epii/epii_animations.py | 1141 ++++++++++++++++++++++++++++++++++ epii/poem_animations.py | 298 +++++++++ generate_logo.py | 44 ++ helpers.py | 140 +++++ images2gif.py | 845 +++++++++++++++++++++++++ mobject.py | 555 +++++++++++++++++ tex_image_utils.py | 70 +++ 13 files changed, 4534 insertions(+) create mode 100644 animate.py create mode 100644 constants.py create mode 100644 displayer.py create mode 100644 epii/__init__.py create mode 100644 epii/cccd_animations.py create mode 100644 epii/dexp_generate_images.py create mode 100644 epii/epii_animations.py create mode 100644 epii/poem_animations.py create mode 100644 generate_logo.py create mode 100644 helpers.py create mode 100644 images2gif.py create mode 100644 mobject.py create mode 100644 tex_image_utils.py diff --git a/animate.py b/animate.py new file mode 100644 index 00000000..619de43d --- /dev/null +++ b/animate.py @@ -0,0 +1,478 @@ +from PIL import Image +from colour import Color +import numpy as np +import warnings +import time +import os +import copy +import progressbar +import inspect +from images2gif import writeGif + + +from helpers import * +from mobject import * +import displayer as disp + +class Animation(object): + def __init__(self, + mobject, + alpha_func = high_inflection_0_to_1, + run_time = DEFAULT_ANIMATION_RUN_TIME, + pause_time = DEFAULT_ANIMATION_PAUSE_TIME, + dither_time = DEFAULT_DITHER_TIME, + name = None): + if isinstance(mobject, type) and issubclass(mobject, Mobject): + self.mobject = mobject() + self.starting_mobject = mobject() + elif isinstance(mobject, Mobject): + self.mobject = mobject + self.starting_mobject = copy.deepcopy(mobject) + else: + raise Exception("Invalid mobject parameter, must be \ + subclass or instance of Mobject") + self.reference_mobjects = [self.starting_mobject] + self.alpha_func = alpha_func or (lambda x : x) + self.run_time = run_time + self.pause_time = pause_time + self.dither_time = dither_time + self.nframes, self.ndither_frames = self.get_frame_count() + self.nframes_past = 0 + self.frames = [] + self.concurrent_animations = [] + self.following_animations = [] + self.reference_animations = [] + self.background_mobjects = [] + self.filter_functions = [] + self.restricted_height = SPACE_HEIGHT + self.restricted_width = SPACE_WIDTH + self.spacial_center = np.zeros(3) + self.name = self.__class__.__name__ + str(self.mobject) + self.inputted_name = name + + def __str__(self): + return self.inputted_name or self.name + + def get_points_and_rgbs(self): + """ + It is the responsibility of this class to only emit points within + the space. Returns np array of points and corresponding np array + of rgbs + """ + points = np.zeros(0) + rgbs = np.zeros(0) + for mobject in self.background_mobjects + [self.mobject]: + points = np.append(points, mobject.points) + rgbs = np.append(rgbs, mobject.rgbs) + #Kind of hacky + if mobject.SHOULD_BUFF_POINTS: #TODO, think about this. + up_nudge = np.array( + (2.0 * SPACE_HEIGHT / HEIGHT, 0, 0) + ) + side_nudge = np.array( + (0, 2.0 * SPACE_WIDTH / WIDTH, 0) + ) + for nudge in up_nudge, side_nudge, up_nudge + side_nudge: + points = np.append(points, mobject.points + nudge) + rgbs = np.append(rgbs, mobject.rgbs) + points = points.reshape((points.size/3, 3)) + rgbs = rgbs.reshape((rgbs.size/3, 3)) + #Filters out what is out of bounds. + admissibles = (abs(points[:,0]) < self.restricted_width) * \ + (abs(points[:,1]) < self.restricted_height) + for filter_function in self.filter_functions: + admissibles *= ~filter_function(points) + if any(self.spacial_center): + points += self.spacial_center + #Filter out points pushed off the edge + admissibles *= (abs(points[:,0]) < SPACE_WIDTH) * \ + (abs(points[:,1]) < SPACE_HEIGHT) + if rgbs.shape[0] < points.shape[0]: + #TODO, this shouldn't be necessary, find what's happening. + points = points[:rgbs.shape[0], :] + admissibles = admissibles[:rgbs.shape[0]] + return points[admissibles, :], rgbs[admissibles, :] + + def update(self): + if self.nframes_past > self.nframes: + return False + self.nframes_past += 1 + for anim in self.concurrent_animations + self.reference_animations: + anim.update() + self.update_mobject(self.alpha_func(self.get_fraction_complete())) + return True + + def while_also(self, action, display = True, *args, **kwargs): + if isinstance(action, type) and issubclass(action, Animation): + self.reference_animations += [ + action(mobject, *args, **kwargs) + for mobject in self.reference_mobjects + [self.mobject] + ] + self.name += action.__name__ + return self + if action.mobject == self.mobject: + #This is just for a weird edge case + action.mobject = self.starting_mobject + new_home = self.concurrent_animations if display else \ + self.reference_animations + new_home.append(action) + self.name += str(action) + return self + + def with_background(self, *mobjects): + for anim in [self] + self.following_animations: + anim.background_mobjects.append(CompoundMobject(*mobjects)) + return self + + def then(self, action, carry_over_background = False, *args, **kwargs): + if isinstance(action, type) and issubclass(action, Animation): + action = action(mobject = self.mobject, *args, **kwargs) + if carry_over_background: + action.background_mobjects += self.background_mobjects + self.following_animations.append(action) + if self.frames: + self.frames += action.get_frames() + self.name += "Then" + str(action) + return self + + def get_image(self): + all_points, all_rgbs = self.get_points_and_rgbs() + for anim in self.concurrent_animations: + new_points, new_rgbs = anim.get_points_and_rgbs() + all_points = np.append(all_points, new_points) + all_rgbs = np.append(all_rgbs, new_rgbs) + all_points = all_points.reshape((all_points.size/3, 3)) + all_rgbs = all_rgbs.reshape((all_rgbs.size/3, 3)) + return disp.get_image(all_points, all_rgbs) + + def generate_frames(self): + print "Generating " + str(self) + "..." + progress_bar = progressbar.ProgressBar(maxval=self.nframes) + progress_bar.start() + + self.frames = [] + while self.update(): + self.frames.append(self.get_image()) + progress_bar.update(self.nframes_past - 1) + self.clean_up() + for anim in self.following_animations: + self.frames += anim.get_frames() + progress_bar.finish() + return self + + def get_fraction_complete(self): + result = float(self.nframes_past - self.ndither_frames) / ( + self.nframes - 2 * self.ndither_frames) + if result <= 0: + return 0 + elif result >= 1: + return 1 + return result + + def get_frames(self): + if not self.frames: + self.generate_frames() + return self.frames + + def get_frame_count(self): + nframes = int((self.run_time + 2*self.dither_time)/ self.pause_time) + ndither_frames = int(self.dither_time / self.pause_time) + return nframes, ndither_frames + + def filter_out(self, *filter_functions): + self.filter_functions += filter_functions + return self + + def restrict_height(self, height): + self.restricted_height = min(height, SPACE_HEIGHT) + return self + + def restrict_width(self, width): + self.restricted_width = min(width, SPACE_WIDTH) + return self + + def shift(self, vector): + self.spacial_center += vector + for anim in self.following_animations: + anim.shift(vector) + return self + + def set_dither(self, time, apply_to_concurrent = False): + self.dither_time = time + if apply_to_concurrent: + for anim in self.concurrent_animations + self.reference_animations: + anim.set_dither(time) + return self.reload() + + def set_run_time(self, time, apply_to_concurrent = False): + self.run_time = time + if apply_to_concurrent: + for anim in self.concurrent_animations + self.reference_animations: + anim.set_run_time(time) + return self.reload() + + def set_alpha_func(self, alpha_func): + if alpha_func is None: + alpha_func = lambda x : x + self.alpha_func = alpha_func + return self + + def set_name(self, name): + self.inputted_name = name + return self + + def reload(self): + self.nframes, self.ndither_frames = self.get_frame_count() + if self.frames: + self.nframes_past = 0 + self.generate_frames() + return self + + def drag_pixels(self): + self.frames = drag_pixels(self.get_frames()) + return self + + def reverse(self): + self.get_frames().reverse() + self.name = 'Reversed' + str(self) + return self + + def write_to_gif(self, name = None): + disp.write_to_gif(self, name or str(self)) + + def write_to_movie(self, name = None): + disp.write_to_movie(self, name or str(self)) + + def update_mobject(self, alpha): + #Typically ipmlemented by subclass + pass + + def clean_up(self): + pass + + def dither(self): + pass + + +###### Concrete Animations ######## + +class Rotating(Animation): + def __init__(self, + mobject, + axis = None, + axes = [[0, 0, 1], [0, 1, 0]], + radians = 2 * np.pi, + run_time = 20.0, + dither_time = 0.0, + alpha_func = None, + *args, **kwargs): + Animation.__init__( + self, mobject, + run_time = run_time, dither_time = dither_time, + alpha_func = alpha_func, + *args, **kwargs + ) + self.axes = [axis] if axis else axes + self.radians = radians + + def update_mobject(self, alpha): + self.mobject.points = self.starting_mobject.points + for axis in self.axes: + self.mobject.rotate( + self.radians * alpha, + axis + ) +class RotationAsTransform(Rotating): + def __init__(self, mobject, radians, + run_time = DEFAULT_ANIMATION_RUN_TIME, + dither_time = DEFAULT_DITHER_TIME, + *args, **kwargs): + Rotating.__init__( + self, + mobject, + axis = (0, 0, 1), + run_time = run_time, + dither_time = dither_time, + radians = radians, + alpha_func = high_inflection_0_to_1, + ) + +class FadeOut(Animation): + def update_mobject(self, alpha): + self.mobject.rgbs = self.starting_mobject.rgbs * (1 - alpha) + +class Reveal(Animation): + def update_mobject(self, alpha): + self.mobject.rgbs = self.starting_mobject.rgbs * alpha + if self.mobject.points.shape != self.starting_mobject.points.shape: + self.mobject.points = self.starting_mobject.points + #TODO, Why do you need to do this? Shouldn't points always align? + +class Transform(Animation): + def __init__(self, mobject1, mobject2, run_time = DEFAULT_TRANSFORM_RUN_TIME, + *args, **kwargs): + count1, count2 = mobject1.get_num_points(), mobject2.get_num_points() + Mobject.align_data(mobject1, mobject2) + Animation.__init__(self, mobject1, run_time = run_time, *args, **kwargs) + self.ending_mobject = mobject2 + self.mobject.SHOULD_BUFF_POINTS = \ + mobject1.SHOULD_BUFF_POINTS and mobject2.SHOULD_BUFF_POINTS + self.reference_mobjects.append(mobject2) + self.name += "To" + str(mobject2) + + if count2 < count1: + #Ensure redundant pixels fade to black + indices = self.non_redundant_m2_indices = \ + np.arange(0, count1-1, float(count1) / count2).astype('int') + temp = np.zeros(mobject2.points.shape) + temp[indices] = mobject2.rgbs[indices] + mobject2.rgbs = temp + + def update_mobject(self, alpha): + Mobject.interpolate( + self.starting_mobject, + self.ending_mobject, + self.mobject, + alpha + ) + + def 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 ApplyMethod(Transform): + def __init__(self, method, mobject, *args, **kwargs): + """ + method is a method of Mobject + """ + method_args = () + if isinstance(method, tuple): + method, method_args = method[0], method[1:] + if not inspect.ismethod(method): + raise "Not a valid Mobject method" + Transform.__init__( + self, + mobject, + method(copy.deepcopy(mobject), *method_args), + *args, **kwargs + ) + +class ApplyFunction(Transform): + def __init__(self, function, mobject, run_time = DEFAULT_ANIMATION_RUN_TIME, + *args, **kwargs): + map_image = copy.deepcopy(mobject) + map_image.points = np.array(map(function, map_image.points)) + Transform.__init__(self, mobject, map_image, run_time = run_time, + *args, **kwargs) + self.name = "".join([ + "Apply", + "".join([s.capitalize() for s in function.__name__.split("_")]), + "To" + str(mobject) + ]) + +class ComplexFunction(ApplyFunction): + def __init__(self, function, *args, **kwargs): + def point_map(point): + x, y, z = point + c = np.complex(x, y) + c = function(c) + return c.real, c.imag, z + if len(args) > 0: + args = list(args) + mobject = args.pop(0) + elif "mobject" in kwargs: + mobject = kwargs.pop("mobject") + else: + mobject = Grid() + ApplyFunction.__init__(self, point_map, mobject, *args, **kwargs) + self.name = "ComplexFunction" + to_cammel_case(function.__name__) + #Todo, abstract away function naming' + +class Homotopy(Animation): + def __init__(self, homotopy, *args, **kwargs): + """ + Homotopy a function from (x, y, z, t) to (x', y', z') + """ + self.homotopy = homotopy + Animation.__init__(self, *args, **kwargs) + + def update_mobject(self, alpha): + self.mobject.points = np.array([ + self.homotopy((x, y, z, alpha)) + for x, y, z in self.starting_mobject.points + ]) + +class ComplexHomotopy(Homotopy): + def __init__(self, complex_homotopy, *args, **kwargs): + """ + Complex Hootopy a function (z, t) to z' + """ + def homotopy((x, y, z, t)): + c = complex_homotopy((complex(x, y), t)) + return (c.real, c.imag, z) + if len(args) > 0: + args = list(args) + mobject = args.pop(0) + elif "mobject" in kwargs: + mobject = kwargs["mobject"] + else: + mobject = Grid() + Homotopy.__init__(self, homotopy, mobject, *args, **kwargs) + self.name = "ComplexHomotopy" + \ + to_cammel_case(complex_homotopy.__name__) + + +class ShowCreation(Animation): + def update_mobject(self, alpha): + new_num_points = int(alpha * self.starting_mobject.points.shape[0]) + for attr in ["points", "rgbs"]: + setattr( + self.mobject, + attr, + getattr(self.starting_mobject, attr)[:new_num_points, :] + ) + +class Flash(Animation): + def __init__(self, mobject, color = "white", slow_factor = 0.01, + run_time = 0.1, dither_time = 0, alpha_func = None, + *args, **kwargs): + Animation.__init__(self, mobject, run_time = run_time, + dither_time = dither_time, + alpha_func = alpha_func, + *args, **kwargs) + self.intermediate = Mobject(color = color) + self.intermediate.add_points([ + point + (x, y, 0) + for point in self.mobject.points + for x in [-1, 1] + for y in [-1, 1] + ]) + self.reference_mobjects.append(self.intermediate) + self.slow_factor = slow_factor + + def update_mobject(self, alpha): + #Makes alpha go from 0 to slow_factor to 0 instead of 0 to 1 + alpha = self.slow_factor * (1.0 - 4 * (alpha - 0.5)**2) + Mobject.interpolate( + self.starting_mobject, + self.intermediate, + self.mobject, + alpha + ) + + + + + + + diff --git a/constants.py b/constants.py new file mode 100644 index 00000000..6dd5e947 --- /dev/null +++ b/constants.py @@ -0,0 +1,36 @@ +import os + +PRODUCTION_QUALITY = True + +DEFAULT_POINT_DENSITY_2D = 25 if PRODUCTION_QUALITY else 20 +DEFAULT_POINT_DENSITY_1D = 200 if PRODUCTION_QUALITY else 50 + +HEIGHT = 1024#1440 if PRODUCTION_QUALITY else 480 +WIDTH = 1024#2560 if PRODUCTION_QUALITY else 640 +#All in seconds +DEFAULT_ANIMATION_PAUSE_TIME = 0.04 if PRODUCTION_QUALITY else 0.1 +DEFAULT_ANIMATION_RUN_TIME = 3.0 +DEFAULT_TRANSFORM_RUN_TIME = 1.0 +DEFAULT_DITHER_TIME = 1.0 + +GENERALLY_BUFF_POINTS = PRODUCTION_QUALITY + +BACKGROUND_COLOR = "black" #TODO, this is never actually enforced anywhere. + +DEFAULT_NUM_STARS = 1000 + +SPACE_HEIGHT = 4.0 +SPACE_WIDTH = WIDTH * SPACE_HEIGHT / HEIGHT + +PDF_DENSITY = 400 + +IMAGE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "images") +GIF_DIR = os.path.join(os.getenv("HOME"), "Desktop", "math_gifs") +MOVIE_DIR = os.path.join(os.getenv("HOME"), "Desktop", "math_movies") +PDF_DIR = os.path.join(os.getenv("HOME"), "Documents", "Tex", "Animations") +TMP_IMAGE_DIR = "/tmp/animation_images/" +for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TMP_IMAGE_DIR]: + if not os.path.exists(folder): + os.mkdir(folder) + +LOGO_PATH = os.path.join(IMAGE_DIR, "logo.png") \ No newline at end of file diff --git a/displayer.py b/displayer.py new file mode 100644 index 00000000..a8181daa --- /dev/null +++ b/displayer.py @@ -0,0 +1,124 @@ +import numpy as np +import itertools as it +import os +from PIL import Image +import cv2 + +from animate import * + + +def get_image(points, rgbs): + return Image.fromarray(get_pixels(points, rgbs)) + +def get_pixels(points, rgbs): + #TODO, Let z add a depth componenet? + points = points[:, :2] + #Flips y-axis + points[:,1] *= -1 + #Map points to pixel space, then create pixel array first in terms + #of its flattened version + points += np.array( + [SPACE_WIDTH, SPACE_HEIGHT]*points.shape[0] + ).reshape(points.shape) + points *= np.array( + [HEIGHT / (2.0 * SPACE_HEIGHT), WIDTH / (2.0 * SPACE_WIDTH)]*\ + points.shape[0] + ).reshape(points.shape) + points = points.astype('int') + flattener = np.array([1, WIDTH], dtype = 'int').reshape((2, 1)) + indices = np.dot(points, flattener) + indices = indices.reshape(indices.size) + admissibles = (indices < HEIGHT * WIDTH) * (indices > 0) + indices = indices[admissibles] + rgbs = rgbs[admissibles] + rgbs = (rgbs * 255).astype(int) + pixels = np.zeros((HEIGHT * WIDTH, 3)) + pixels[indices] = rgbs + return pixels.reshape((HEIGHT, WIDTH, 3)).astype('uint8') + +def write_to_gif(animation, name): + #TODO, find better means of compression + if not name.endswith(".gif"): + name += ".gif" + filepath = os.path.join(GIF_DIR, name) + temppath = os.path.join(GIF_DIR, "Temp.gif") + print "Writing " + name + "..." + writeGif(temppath, animation.get_frames(), animation.pause_time) + print "Compressing..." + os.system("gifsicle -O " + temppath + " > " + filepath) + os.system("rm " + temppath) + +def write_to_movie(animation, name): + frames = animation.get_frames() + progress_bar = progressbar.ProgressBar(maxval=len(frames)) + progress_bar.start() + print "writing " + name + "..." + + tmp_stem = os.path.join(TMP_IMAGE_DIR, name.replace("/", "_")) + suffix = "-%04d.png" + image_files = [] + for frame, count in zip(frames, it.count()): + progress_bar.update(int(0.9 * count)) + frame.save(tmp_stem + suffix%count) + image_files.append(tmp_stem + suffix%count) + filepath = os.path.join(MOVIE_DIR, name + ".mp4") + filedir = "/".join(filepath.split("/")[:-1]) + if not os.path.exists(filedir): + os.makedirs(filedir) + commands = [ + "ffmpeg", + "-y", + "-loglevel", + "error", + "-i", + tmp_stem + suffix, + "-c:v", + "libx264", + "-vf", + "fps=%d,format=yuv420p"%int(1/animation.pause_time), + filepath + ] + os.system(" ".join(commands)) + for image_file in image_files: + os.remove(image_file) + progress_bar.finish() + + + # filepath = os.path.join(MOVIE_DIR, name + ".mov") + # fourcc = cv2.cv.FOURCC(*"8bps") + # out = cv2.VideoWriter( + # filepath, fourcc, 1.0/animation.pause_time, (WIDTH, HEIGHT), True + # ) + # progress = 0 + # for frame in frames: + # if progress == 0: + # print "Writing movie" + # progress_bar.update(progress) + # r, g, b = cv2.split(np.array(frame)) + # bgr_frame = cv2.merge([b, g, r]) + # out.write(bgr_frame) + # progress += 1 + # out.release() + # progress_bar.finish() + + + + + + + + + + + + + + + + + + + + + + diff --git a/epii/__init__.py b/epii/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/epii/cccd_animations.py b/epii/cccd_animations.py new file mode 100644 index 00000000..4c8e0f6f --- /dev/null +++ b/epii/cccd_animations.py @@ -0,0 +1,787 @@ +from PIL import Image +from animate import * +from mobject import * +from constants import * +from helpers import * +from tex_image_utils import load_pdf_images +from displayer import * +import itertools as it +import os +import numpy as np +from copy import deepcopy + +from epii_animations import MULTIPLIER_COLOR, ADDER_COLOR, ONE_COLOR + +CCCD_MOVIE_DIR = "cccd" + +symbol_images = load_pdf_images("cccd_symbols.pdf", regen_if_exists = False) +phrase_images = load_pdf_images("cccd_phrases.pdf", regen_if_exists = False) + +name_to_image = dict( + zip([ + "two", + "minus_1", + "i", + "x_squared", + "four", + "one", + "multiplication_function", + "deriv_def_base", + "deriv_def_inner_e_to_x", + "deriv_def_plus_h", + "deriv_def_e_to_h", + "deriv_def_one", + "deriv_def_outer_e_to_x", + "series_terms", + "series_exponents", + "series_exponents_minus_1", + "d_series_coefficients", + "one_plus", + "d_series_simple", + "deriv_x_squared", + "deriv_e_to_x", + "question_mark", + ], symbol_images) + zip([ + "complex_derivative_title", + "limit_explanation", + "velocity_vector_explanation", + "both_same_point", + "maybe_like_this", + "or_this", + "why_vectors", + "pause_and_ponder", + "think_in_pictures", + "remember_this", + ], phrase_images) +) + +def function_of_numbers(): + kwargs = {"dither_time" : 0} + two, minus_1, i, x_squared, four, one = [ + ImageMobject(name_to_image[name]) + for name in ["two", "minus_1", "i", "x_squared", + "four", "one"] + ] + minus_1_copy = copy.deepcopy(minus_1) + for mob1, mob2, height in [ + (two, four, 2), + (minus_1, one, 0), + (i, minus_1_copy, -2) + ]: + mob1.center().shift((-2, height, 0)) + mob2.center().shift((2, height, 0)) + x_squared.center() + point = Point() + inputs = CompoundMobject(two, minus_1, i) + outputs = CompoundMobject(four, one, minus_1_copy) + return Transform( + inputs, point, **kwargs + ).then( + Transform(point, outputs, **kwargs) + ).with_background(x_squared) + + +def real_function_graph(): + graph = NumberLine() + graph.add(NumberLine().rotate(np.pi / 2)) + int_size = graph.interval_size + min_x = SPACE_WIDTH / int_size + graph.add(FunctionGraph( + lambda x : x**2, + x_range = [-min_x, min_x] + )) + point = graph.points[-20,:] + line = Line((2, 0, 0), (2, 1.1, 0)) #Terrible... + line.highlight("yellow") + return ShowCreation(graph).then(ShowCreation(line).with_background(graph)) + +def two_grids(): + grid1, grid2 = Grid(), Grid() + return ShowCreation(grid1).then( + Rotating(grid1, run_time = 7.0, radians = np.pi / 3).while_also( + ShowCreation(grid2, dither_time = 2.0).while_also( + Rotating, axis = [1, 1, 0] + ) + ) + ) + +def z_squared(): + return ComplexHomotopy(square_homotopy) + +def z_squared_marked(): + #Hard coded 2, i, -1 + return ComplexHomotopy( + lambda (z, t) : z**(1 + t) + ).while_also( + ComplexHomotopy( + lambda (z, t) : z + complex(2, 0)**(1 + t), + Cross( + color = random_color() + ) + ) + ).while_also( + ComplexHomotopy( + lambda (z, t) : z + complex(-1, 0)**(1 + t), + Cross( + color = random_color() + ) + ) + ).while_also( + ComplexHomotopy( + lambda (z, t) : z + complex(0, 1)**(1 + t), + Cross( + color = random_color() + ) + ) + ) + +def multiplier_in_the_wild(point): + func = ComplexFunction(lambda z : z*point) + one = Cross().highlight(ONE_COLOR).shift((1, 0, 0)) + point = Cross().highlight( + MULTIPLIER_COLOR + ).shift((point.real, point.imag, 0)) + func_copy = copy.deepcopy(func) + return func.then( + func_copy.while_also( + Transform(one, point, run_time = DEFAULT_ANIMATION_RUN_TIME) + ) + ).then( + Animation( + Grid(), run_time = 2.0, dither_time = 0 + ).with_background(point) + ) + +def random_looking_function(): + wongky_map = Transform( + Grid(), CubeShell().scale(SPACE_HEIGHT), + alpha_func = lambda t : 0.5 * high_inflection_0_to_1(t), + run_time = 3.0 + ) + return wongky_map.then(copy.deepcopy(wongky_map).reverse()) + +def zoom_in_on_map(function, is_homotopy, point, zoom_level): + center_line = ParametricFunction(lambda t : (0, t * SPACE_HEIGHT, 0)) + half_width = SPACE_WIDTH / 2 + left_center = (-half_width + center_line.epsilon, 0, 0) + right_center = (half_width - center_line.epsilon, 0, 0) + left_divider = copy.deepcopy(center_line).shift(right_center) + right_divider = copy.deepcopy(center_line).shift(left_center) + + point = complex(point) + outer_circle = Circle().scale(SPACE_HEIGHT + SPACE_WIDTH + half_width) + inner_circle = Circle().scale(zoom_level).shift( + (point.real, point.imag, 0) + ) + outer_to_inner = Transform(outer_circle, inner_circle).with_background(Grid()) + big_radius = min(half_width, SPACE_HEIGHT) + big_circle = Circle().scale(big_radius) + bts_ratio = big_radius / zoom_level + half_grid = Grid().filter_out(lambda (x, y, z) : abs(x) > half_width or y > SPACE_HEIGHT) + one = Cross().shift((1, 0, 0)).highlight(ONE_COLOR) + if is_homotopy: + global_function = ComplexHomotopy(function, copy.deepcopy(half_grid)) + def local_homotopoy((z, t)): + return bts_ratio * (function((z/bts_ratio + point, t)) - function((point,t))) + local_function = ComplexHomotopy(local_homotopoy, copy.deepcopy(half_grid)) + one_following = ComplexHomotopy( + lambda (z, t) : z + local_homotopoy((1, t)) - 1, + one + ) + circle_following = ComplexHomotopy( + lambda (z, t) : z + function((point, t)) - point, + inner_circle + ) + else: + global_function = ComplexFunction(function, copy.deepcopy(half_grid)) + def local_lambda(z): + return bts_ratio*(function(z/bts_ratio + point) - function(point)) + local_function = ComplexFunction(local_lambda, copy.deepcopy(half_grid)) + one_following = ComplexFunction(lambda z : z + local_lambda(1) - 1, one) + circle_following = ComplexFunction( + lambda z : z + function(point) - point, + inner_circle + ) + zoom_region = Circle().scale(zoom_level) + zoom_region.add(Grid().filter_out(lambda p : np.linalg.norm(p) > zoom_level)) + zoom_region.shift(left_center).shift((point.real, point.imag, 0)) + zoom_in = ComplexFunction( + lambda z : bts_ratio * (z - point - complex(left_center[0], left_center[1])) + \ + complex(right_center[0], right_center[1]), + zoom_region + ).set_run_time(DEFAULT_TRANSFORM_RUN_TIME) + + grow_local_grid = ShowCreation( + Grid().filter_out(lambda p : np.linalg.norm(p) > big_radius), + run_time = 1.0 + ).with_background(big_circle) + def out_of_circle(points): + return np.apply_along_axis(np.linalg.norm, 1, points) > big_radius + local_function.filter_out(out_of_circle) + for anim in global_function, outer_to_inner, circle_following: + anim.shift(left_center).restrict_width(half_width).with_background(left_divider) + for anim in local_function, grow_local_grid, one_following: + anim.shift(right_center).with_background(right_divider, big_circle) + for anim in outer_to_inner, zoom_in, grow_local_grid: + anim.set_dither(0) + #Kind of hacky...one day there will be a better way of doing this. + show_left_grid = Animation(Mobject().add_points(*global_function.get_points_and_rgbs())) + show_left_grid.with_background(copy.deepcopy(inner_circle).shift(left_center)) + return outer_to_inner.then( + zoom_in.while_also(show_left_grid), + ).then( + grow_local_grid.while_also(show_left_grid).while_also(zoom_in) + ).then( + global_function.while_also( + local_function + ).while_also( + circle_following + ).while_also( + one_following + ) + ) + +def z_squared_derivative_example(point, zoom_level): + point = complex(point) + point_coords = np.array((point.real, point.imag, 0)) + circle = Circle().scale(zoom_level).shift(point_coords) + z = Cross(color = Circle.DEFAULT_COLOR).shift(point_coords) + two_z = Cross(color = MULTIPLIER_COLOR).shift(2*point_coords) + zero = Cross() + one = Cross(color = ONE_COLOR).shift((1, 0, 0)) + plane = Grid() + return Transform(circle, z).with_background(plane, zero).then( + Transform(z, two_z).with_background( + plane, zero + ).while_also( + Reveal(one).with_background( + two_z, plane, zero + ) + ).set_dither(0) + ).then( + ComplexFunction(lambda c : c * 2 * point, plane).with_background( + two_z, zero + ).while_also( + ComplexFunction(lambda c : c - 1 + 2 * point, one) + ) + ) + +def z_squared_derivative_2z(zoom_level): + circles = Mobject() + crosses = Mobject() + mini_maps = [] + mini_grids = Mobject() + example_range = range(-3, 4, 2) + for x in example_range: + for y in example_range: + circle = Circle().scale(zoom_level).shift((x, y, 0)) + cross = Cross().shift((2*x, 2*y, 0)) + mini_grid = Grid( + radius = zoom_level, + subinterval_size = 0.25 + ).filter_out( + lambda point : np.linalg.norm(point) > zoom_level + ) + mini_maps.append( + ComplexFunction( + lambda z : (z + complex(x, y))**2 - complex(x, y)**2, + mini_grid + ).filter_out( + lambda points : np.apply_along_axis(np.linalg.norm, 1, points) \ + > zoom_level + ).with_background( + Circle().scale(zoom_level) + ).shift((x, y, 0)).set_dither(0) + )#generate frames so that lambda doesn't change + mini_grids.add(copy.deepcopy(mini_grid).shift((x, y, 0))) + Mobject.align_data(circle, cross) + circles.add(circle) + crosses.add(cross) + all_mini_maps = reduce(Animation.while_also, mini_maps) + crosses.highlight(MULTIPLIER_COLOR) + return FadeOut(Grid()).while_also( + Animation(CompoundMobject(circles, mini_grids)) + ).then( + all_mini_maps.with_background(circles) + ).then( + ComplexFunction(lambda z : 2*z).while_also( + Transform( + circles, crosses, + run_time = DEFAULT_ANIMATION_RUN_TIME + ) + ) + ) + + +def visualize_exp(): + kwargs1 = {"run_time" : 2.0, "dither_time" : 1.0} + kwargs2 = {"run_time" : 2.0, "dither_time" : 0.0} + cylinder = Grid().apply_function( + lambda (x, y, z) : (x, np.sin(y), np.cos(y)) + ).rotate(np.pi/9, [0, 1, 0]) + exp_plane = Grid().apply_complex_function(np.exp) + rotating_cyl = Rotating(cylinder, radians = np.pi/5, run_time = 5.0) + return Transform(Grid(), cylinder, **kwargs1).then( + Transform(cylinder, exp_plane, **kwargs2) + ) + +def derivative_definition(): + base, inner_e_to_x, plus_h, e_to_h, one, outer_e_to_x = [ + ImageMobject(name_to_image["deriv_def_" + name]) + for name in [ + "base", + "inner_e_to_x", + "plus_h", + "e_to_h", + "one", + "outer_e_to_x", + ] + ] + shift = (-0.2, -0.2, 0) + base.shift(shift) + outer_e_to_x.shift(shift) + limit_explanation = ImageMobject(name_to_image["limit_explanation"]) + limit_explanation.shift((1, -2, 0)) + return Transform(plus_h, e_to_h).with_background( + base, inner_e_to_x + ).then( + Transform(inner_e_to_x, outer_e_to_x).while_also( + Reveal(one, dither_time = 1.0, run_time = 1.0) + ).with_background(base, e_to_h) + ).then( + Reveal(limit_explanation).with_background( + base, outer_e_to_x, one, e_to_h + ) + ) + + +def take_derivative_of_series(): + series_terms, series_exponents, series_exponents_minus_1, \ + d_series_coefficients, one_plus, d_series_simple = [ + ImageMobject(name_to_image[name]) + for name in [ + "series_terms", + "series_exponents", + "series_exponents_minus_1", + "d_series_coefficients", + "one_plus", + "d_series_simple", + ] + ] + coefficients = copy.deepcopy(series_exponents) + fraction_bars = copy.deepcopy(series_exponents) + coefficients.filter_out(lambda (x, y, z) : y < 0.5) + series_exponents_minus_1.filter_out(lambda (x, y, z) : y < 0.5) + fraction_bars.filter_out(lambda (x, y, z) : y > 0.5) + exponenets_to_coefficients = Homotopy( + lambda (x, y, z, t) : (x - 0.5*t, y - 0.5*t + np.sin(np.pi * t), z), + coefficients + ).with_background(fraction_bars) + d_series_non_simple = CompoundMobject( + series_terms, + copy.deepcopy(coefficients).shift((-0.5, -0.5, 0)), + series_exponents_minus_1, + series_exponents + ) + d_series_simple.center().shift((1, 0, 0)) + + #Yeah, this is totally good programming... + fdxc = [-2, -0.4, 1.3] #first dividing x coordinates + sdxc = [-1.7, -0.8, .3] #second dividing x coordinates + broken_series_terms = [ + copy.deepcopy(d_series_non_simple).filter_out( + lambda (x, y, z) : x > fdxc[0]), + copy.deepcopy(d_series_non_simple).filter_out( + lambda (x, y, z) : x > fdxc[1] or x < fdxc[0]), + copy.deepcopy(d_series_non_simple).filter_out( + lambda (x, y, z) : x > fdxc[2] or x < fdxc[1]), + copy.deepcopy(d_series_non_simple).filter_out( + lambda (x, y, z) : x < fdxc[2]), + ] + broken_dseries_terms = [ + copy.deepcopy(d_series_simple).filter_out( + lambda (x, y, z) : x > sdxc[0]), + copy.deepcopy(d_series_simple).filter_out( + lambda (x, y, z) : x > sdxc[1] or x < sdxc[0]), + copy.deepcopy(d_series_simple).filter_out( + lambda (x, y, z) : x > sdxc[2] or x < sdxc[1]), + copy.deepcopy(d_series_simple).filter_out( + lambda (x, y, z) : x < sdxc[2]), + ] + simplify = None + for term1, term2 in zip(broken_series_terms, broken_dseries_terms): + anim = Transform(term1, term2) + if simplify: + simplify.while_also(anim) + else: + simplify = anim + series_terms.add(series_exponents) + return exponenets_to_coefficients.while_also( + Reveal(series_exponents_minus_1).with_background( + series_terms, series_exponents + ) + ).while_also( + Transform(one_plus, Point(one_plus.get_center())) + ).set_dither(1.0, True).set_run_time(1.0, True).then( + simplify + ) + +def e_to_x_takes_adder_to_multiplier(point): + point = complex(point) + point_coords = (point.real, point.imag, 0) + image = np.exp(point) + image_coords = (image.real, image.imag, 0) + adder_cross = Cross().shift(point_coords).highlight(ADDER_COLOR) + multi_cross = Cross().shift(image_coords).highlight(MULTIPLIER_COLOR) + zero = Cross() + one = Cross().shift((1, 0, 0)).highlight(ONE_COLOR) + + adder = ComplexFunction( + lambda z : z + point, + CompoundMobject(Grid(radius = SPACE_WIDTH + SPACE_HEIGHT), zero) + ).with_background(adder_cross) + e_to_x = ComplexFunction(np.exp).while_also( + Transform(adder_cross, multi_cross, run_time = DEFAULT_ANIMATION_RUN_TIME) + ) + multiplier = ComplexFunction(lambda z : image * z).while_also( + Transform(one, multi_cross, run_time = DEFAULT_ANIMATION_RUN_TIME) + ).with_background(zero, multi_cross) + return adder.then(e_to_x).then(multiplier) + +def e_to_x_derivative_zoom(point, zoom_level): + point = complex(point) + image_point = np.exp(point) + both_same_point = ImageMobject( + name_to_image["both_same_point"] + ).shift((image_point.real, SPACE_HEIGHT - 1, 0)) + left_point = (image_point.real - SPACE_WIDTH/2, image_point.imag, 0) + right_point = (image_point.real + SPACE_WIDTH/2, image_point.imag, 0) + left_arrow = Arrow(left_point, (-1, -1, 0)).highlight("white") + right_arrow = Arrow(right_point, (1, -1, 0)).highlight("white") + + e_deriv_anim = zoom_in_on_map(np.exp, False, point, zoom_level) + #SUPER HACKY, YOU MUST MAKE A BETTER WAY TO DO THIS IN FUTURE + last_anim = e_deriv_anim.generate_frames().following_animations[-1] + background = Mobject() + for anim in [last_anim] + last_anim.concurrent_animations: + background.add_points(*anim.get_points_and_rgbs()) + # background.display() + return e_deriv_anim.then( + Reveal(CompoundMobject( + left_arrow, right_arrow, both_same_point + )).with_background(background) + ) + +def other_possible_functions(): + phrases = [ + ImageMobject(name_to_image[name]).center().shift((0, -2, 0)) + for name in ["maybe_like_this", "or_this"] + ] + return ComplexFunction(np.sin).with_background( + phrases[0] + ).then( + ComplexFunction(np.sinc).with_background( + phrases[1] + ) + ) + +def setup_velocity_vector_discussion(zoom_level): + def homotopy((x, y, z, t)): + t = 3*t - 1.5 + return ( + x + t, + y + t**3 - t, + z + ) + big_radius = SPACE_HEIGHT + def out_of_circle(points): + return np.apply_along_axis(np.linalg.norm, 1, points) > big_radius + landing_point = homotopy((0, 0, 0, 1)) + cross = Cross().highlight(Circle.DEFAULT_COLOR) + small_circle = Circle().scale(2*zoom_level) + big_circle = Circle().scale(big_radius) + new_cross = copy.deepcopy(cross) + for mob in new_cross, small_circle: + mob.shift(landing_point) + wandering = Homotopy(homotopy, cross) + one = Cross().highlight(ONE_COLOR).shift((1, 0, 0)) + multiply = ComplexFunction( + lambda z : z * complex(landing_point[0], landing_point[1]) + ).filter_out(out_of_circle) + return wandering.then( + Transform(small_circle, big_circle).with_background( + new_cross + ) + ).then( + multiply.with_background(big_circle).while_also( + Transform(one, new_cross, run_time = DEFAULT_ANIMATION_RUN_TIME) + ).with_background( + new_cross + ) + ) + + +def traced_path(): + path = ParametricFunction( + lambda t : ( + np.sin(2 * np.pi * t), + (t-1)**2 + 1, + 0 + ) + ) + new_path = copy.deepcopy(path).apply_complex_function(np.exp) + return ShowCreation(path).then( + Transform(path, new_path) + ) + + +def walking_north(start_point, vector_len): + vv_explanation = ImageMobject(name_to_image["velocity_vector_explanation"]) + vv_explanation.scale(0.75).shift((0, -3, 0)) + + walk_kwargs = {"alpha_func" : None, "run_time" : 5.0} + start_point = complex(start_point) + end_point = start_point + complex(0, 4) + start_coords = (start_point.real, start_point.imag, 0) + end_coords = (end_point.real, end_point.imag, 0) + vector = Vector((0, vector_len, 0)).shift(start_coords) + vector.add(vv_explanation) + start_cross = Cross().shift(start_coords) + end_cross = Cross().shift(end_coords) + path = Line(start_coords, end_coords).highlight(start_cross.DEFAULT_COLOR) + return Transform( + start_cross, end_cross, **walk_kwargs + ).with_background(Grid()).while_also( + ShowCreation(path, **walk_kwargs) + ).then( + Animation(vector), True + ) + +def map_north_vector(start_point, vector_len, zoom_level): + question_mark = ImageMobject(name_to_image["question_mark"]).center() + kwargs = { + "run_time" : DEFAULT_ANIMATION_RUN_TIME, + "dither_time" : DEFAULT_DITHER_TIME + } + start_point = complex(start_point) + image_point = np.exp(start_point) + start_coords = np.array((start_point.real, start_point.imag, 0)) + image_coords = np.array((image_point.real, image_point.imag, 0)) + + vector = Vector((0, vector_len, 0)).add(Circle().scale(zoom_level)) + vimage = copy.deepcopy(vector) + image_len = np.linalg.norm(image_coords) + image_arg = np.log(image_point).imag + stretched = copy.deepcopy(vimage).scale(image_len) + + vector.shift(start_coords) + for vect in vimage, stretched: + vect.shift(image_coords) + question_mark.shift(image_coords + (0.3, 0, 0)) + + line_to_image = Line((0, 0, 0), image_coords) + line_along_horiz = Line((0, 0, 0), (image_len, 0, 0)).highlight(Grid.DEFAULT_COLOR) + + return Transform(vector, vimage).then( + Reveal(question_mark, dither_time = 0).with_background(vimage) + ).then( + Transform(vimage, stretched, **kwargs).while_also( + ShowCreation(line_to_image, **kwargs) + ) + ).with_background(Grid()).then( + RotationAsTransform( + copy.deepcopy(stretched).shift(-image_coords), + radians = image_arg, **kwargs + ).shift(image_coords).while_also( + RotationAsTransform( + line_along_horiz, radians = image_arg, **kwargs + ).with_background(Grid(), line_to_image) + ) + ) + +def all_possible_vectors(vector_len): + tvkwargs = {"run_time" : 2.0, "dither_time" : 0} + turn_vectors = Animation(Grid(), **tvkwargs) + prototype = Vector((0, vector_len, 0)) + start_vectors = Mobject() + final_vectors = Mobject() + radii = [] + example_range = range(-3, 4) + for x, y in it.product(*[example_range]*2): + length = np.linalg.norm((x, y)) + arg = np.log(complex(x, y)).imag + new = copy.deepcopy(prototype) + turn_vectors.while_also( + ComplexFunction( + lambda z : z * complex(x, y), + new, **tvkwargs + ).shift((x, y, 0)) + ) + if length > 0: + radii.append(length) + start_vectors.add(copy.deepcopy(new).shift((x, y, 0))) + final_vectors.add(copy.deepcopy(new).scale(length).rotate(arg).shift((x, y, 0))) + to_vectors = Transform(prototype, start_vectors, **tvkwargs).with_background(Grid()) + turn_vectors.set_name("TurnVectors") + radii = sorted(set(radii)) + show_all_circles = Reveal(CompoundMobject(*[ + Circle().scale(radius) for radius in radii + ]), dither_time = 2.0) + show_all_circles.with_background(Grid(), final_vectors) + show_all_circles.set_name("ShowAllCircles") + return to_vectors.then(turn_vectors).then(show_all_circles) + +class VelocityVectorsOfPath(Animation): + def __init__(self, path, vector_len = 1, alpha_func = None, *args, **kwargs): + self.path = path.points + diffs = path.points[1:,:] - path.points[:-1, :] + self.unit_distance = np.mean(np.apply_along_axis(np.linalg.norm, 1, diffs)) + Animation.__init__( + self, Vector(point = (vector_len, 0, 0)), alpha_func = alpha_func + ) + self.with_background(path) + + def update_mobject(self, alpha): + index = int(alpha * self.path.shape[0]) + if index >= self.path.shape[0] - 1: + return + point1, point2 = self.path[index, :], self.path[index + 1, :] + diff = (point2 - point1) + distance = np.linalg.norm(diff) + arg = np.log(complex(diff[0], diff[1])).imag + self.mobject.points = self.starting_mobject.points * (distance / self.unit_distance) + self.mobject.rotate(arg).shift(point1) + + +def map_trajectories(vector_len, path_func): + half_width = SPACE_WIDTH / 2 + left_center = np.array((-half_width, 0, 0)) + right_center = np.array((half_width, 0, 0)) + dividing_line = Line((half_width, SPACE_HEIGHT, 0), (half_width, -SPACE_HEIGHT, 0)) + left_grid = Grid(radius = SPACE_HEIGHT) + left_path = ParametricFunction(path_func).highlight("white") + right_path = copy.deepcopy(left_path).apply_complex_function(np.exp) + right_grid = copy.deepcopy(left_grid).apply_complex_function(np.exp) + for grid in left_grid, right_grid: + grid.filter_out( + lambda (x, y, z) : abs(x) > half_width + ) + left_start = Cross().shift(left_path.points[0, :]) + right_start = Cross().shift(right_path.points[0, :]) + + apply_function = ComplexFunction( + np.exp, copy.deepcopy(left_grid) + ).restrict_width(half_width + 0.1) #STUPID HACK + move_right_grid = ComplexFunction( + lambda z : z + SPACE_WIDTH, + copy.deepcopy(right_grid).shift(left_center), + run_time = DEFAULT_TRANSFORM_RUN_TIME + ).with_background(copy.deepcopy(left_grid).shift(left_center)) + draw_left_path = ShowCreation(left_path) + draw_right_path = ShowCreation(right_path) + show_left_start = Reveal(left_start) + show_right_start = Reveal(right_start) + left_vectors = VelocityVectorsOfPath(left_path, vector_len) + right_vectors = VelocityVectorsOfPath(right_path, vector_len) + + for anim in apply_function, draw_left_path, show_left_start, left_vectors: + anim.shift(left_center).with_background( + left_grid, dividing_line + ) + for anim in draw_right_path, show_right_start, right_vectors: + anim.shift(right_center).with_background(right_grid) + for anim in draw_left_path, draw_right_path, show_left_start, show_right_start: + anim.set_dither(0) + for anim, bg in (show_left_start, left_path), (show_right_start, right_path): + anim.set_alpha_func(there_and_back).with_background(bg) + left_vectors.with_background(left_path) + right_vectors.with_background(right_path) + return apply_function.then(move_right_grid).then( + draw_left_path.while_also(draw_right_path) + ).then( + show_left_start.while_also(show_right_start) + ).then( + left_vectors.while_also(right_vectors) + ) + + + +if __name__ == '__main__': + example_complex = complex(1, 1) + example_complex2 = complex(2, -1) + zoom_level = 0.5 + strong_zoom_level = 0.1 + vector_len = 0.5 + def square_homotopy((z, t)): + return z**(1 + t) + def example_walk(t): + return ((t + 1)/2, ((t + 1)/2)**2, 0) + def walk_imaginary_axis(t): + return (0, np.pi * (t + 1), 0) + functions = [ + # function_of_numbers, + # real_function_graph, + # two_grids, + # z_squared, + # z_squared_marked, + # (multiplier_in_the_wild, [example_complex2]), + # random_looking_function, + # (zoom_in_on_map, [square_homotopy, True, example_complex, zoom_level]), + # (zoom_in_on_map, [square_homotopy, True, example_complex, strong_zoom_level]), + # (z_squared_derivative_example, [example_complex, zoom_level]), + # (z_squared_derivative_2z, [zoom_level]), + # visualize_exp, + # take_derivative_of_series, + # derivative_definition, + # (e_to_x_takes_adder_to_multiplier, [example_complex]), + # (e_to_x_derivative_zoom, [example_complex, strong_zoom_level]), + # other_possible_functions, + # (setup_velocity_vector_discussion, [strong_zoom_level]), + # traced_path, + # (walking_north, [example_complex, vector_len]), + # (map_north_vector, [example_complex, vector_len, strong_zoom_level]), + # (all_possible_vectors, [vector_len]), + # (map_trajectories, [vector_len, example_walk]), + # (map_trajectories, [vector_len, walk_imaginary_axis]) + ] + + full_path = os.path.join(MOVIE_DIR, CCCD_MOVIE_DIR) + if not os.path.exists(full_path): + os.mkdir(full_path) + for func in functions: + args = [] + if isinstance(func, tuple): + func, args = func + name = os.path.join( + CCCD_MOVIE_DIR, + to_cammel_case(func.__name__) + hash_args(args) + ) + func(*args).write_to_movie(name) + + for anim in [ + # ComplexFunction(lambda z : 0.1*(z**3 - z**2 + 3)), + # ComplexFunction(np.exp, Grid(radius = SPACE_HEIGHT)), + ]: + anim.write_to_movie(os.path.join(CCCD_MOVIE_DIR, str(anim))) + + for name in [ + # "complex_derivative_title", + # "why_vectors", + # "pause_and_ponder", + # "think_in_pictures", + # "remember_this", + # "deriv_x_squared", + # "deriv_e_to_x", + # "multiplication_function", + ]: + ImageMobject(name_to_image[name]).center().save_image( + os.path.join(CCCD_MOVIE_DIR, to_cammel_case(name)) + ) + + + + + + + + diff --git a/epii/dexp_generate_images.py b/epii/dexp_generate_images.py new file mode 100644 index 00000000..4779c236 --- /dev/null +++ b/epii/dexp_generate_images.py @@ -0,0 +1,16 @@ +from PIL import Image +import itertools as it + +from constants import * +from helpers import invert_image +from tex_image_utils import load_pdf_images + + +if __name__ == "__main__": + folder = os.path.join(MOVIE_DIR, "dexp") + if not os.path.exists(folder): + os.makedirs(folder) + images = load_pdf_images("discover_exp.pdf", regen_if_exists = False) + for image, count in zip(images, it.count()): + filepath = os.path.join(folder, "dexp-%d.png"%count) + invert_image(image).save(filepath) diff --git a/epii/epii_animations.py b/epii/epii_animations.py new file mode 100644 index 00000000..83f85077 --- /dev/null +++ b/epii/epii_animations.py @@ -0,0 +1,1141 @@ +#!/usr/bin/env python + +from PIL import Image +from animate import * +from mobject import * +from constants import * +from helpers import * +from tex_image_utils import load_pdf_images +from displayer import * +import itertools as it +import os +import numpy as np +from copy import deepcopy + + +PI_COLOR = "red" +E_COLOR = "skyblue" +I_COLOR = "green" +ADDER_COLOR = "limegreen" +MULTIPLIER_COLOR = "yellow" +ONE_COLOR = "skyblue" + +EPII_MOVIE_DIR = "epii" + +symbol_images = load_pdf_images("epii_symbols.pdf", regen_if_exists = False) +phrase_images = load_pdf_images("epii_phrases.pdf", regen_if_exists = False) + +name_to_image = dict( + zip([ + "e", + "pi", + "i", + "equals_neg1", + "pile_of_equations", + "two", + "plus", + "three", + "four", + "times", + "five", + "fours_underbrace", + "e_to_x", + "e_by_e_x_times", + "e_by_e_pi_i_times", + "e_to_x_property", + "e_to_x_series", + "e_to_5", + "e_to_sum_ones", + "e_to_1_product", + "e_to_x_plus_y", + "e_by_e_x_plus_y_times", + "x_es_then_y_es", + "e_to_x_e_to_y", + "i_squared_equals_neg_1", + "epii_series", + "e_to_complex", + "e_to_matrix", + "e_to_derivative", + "adder_to_multiplier_property", + "defining_property", + "e_by_e", + "e_by_e_by_e", + "e_def", + "e_approx", + "one", + "sqrt_neg1", + "e_digits", + "twenty", + "three_point_five", + "sqrt_two", + "six", + "e_to_2_value", + "e_to_3_value", + "e_to_5_value", + "zero", + "minus", + "neg_1", + "to_the_x", + ], symbol_images) + zip([ + "e_question", + "pi_question", + "i_question", + "what", + "just_trust_it", + "ish", + "question_marks", + "not_what_is_happening", + "what_it_means", + "why_it_works", + "why_it_is_intuitive", + "colon_explanation", + "adder", + "multiplier", + "other_way_around", + "how_do_we_define_this", + "unlearn_what_you_have_learned", + "e_in_nature", + ], phrase_images) +) + +def logo_to_epii(): + e, pi, i, equals_neg1 = [ + ImageMobject(name_to_image[name]) + for name in [ + "e", "pi", "i", "equals_neg1" + ] + ] + epii_neg1 = CompoundMobject(e, pi, i, equals_neg1) + epii_neg1.center().shift((0, 2, 0)) + # stars = Stars() + return Transform( + ImageMobject(LOGO_PATH, invert = False), + epii_neg1, + dither_time = 0, run_time = 2.0, + ).then( + Animation(epii_neg1) + ) + # .then( + # Transform( + # stars, + # epii_neg1, + # dither_time = 0, run_time = 3.0, + # ) + + +def write_epii(): + e, pi, i, equals_neg1 = [ + ImageMobject(name_to_image[name]) + for name in ["e", "pi", "i", "equals_neg1"] + ] + center = CompoundMobject(e, pi, i, equals_neg1).get_center() + for mob in e, pi, i, equals_neg1: + mob.shift(-center) + anims=[ + ShowCreation(e), + ShowCreation(pi).with_background(e), + ShowCreation(i).with_background(e, pi), + ShowCreation(equals_neg1).with_background(e, pi, i), + ] + for anim in anims: + anim.set_run_time(1.0) + for anim in anims[1:]: + anim.set_dither(1) + anims[0].then(anim) + return anims[0] + + +def the_terms(): + e, pi, i, one, e_digits, sqrt_neg1, equals_neg1 = [ + ImageMobject(name_to_image[name]) + for name in [ + "e", "pi", "i", "one", "e_digits", + "sqrt_neg1", "equals_neg1" + ] + ] + center = CompoundMobject(e, pi, i, equals_neg1).get_center() + for mob in [e, pi, i, equals_neg1]: + mob.shift(-center + (0, 2, 0)) + colored_e = deepcopy(e).highlight(E_COLOR) + colored_pi = deepcopy(pi).highlight(PI_COLOR) + colored_i = deepcopy(i).highlight(I_COLOR) + + e_digits.highlight(E_COLOR) + sqrt_neg1.highlight(I_COLOR) + + e_digits.shift([2, -2, 0]) + sqrt_neg1.shift([2, 2, 0]) + + pi_copy = deepcopy(colored_pi) + pi_copy.scale(1.5) + pi_copy.center() + pi_copy.shift([-0.8, 0, 0]) + + one.center() + one.shift([0.2, 0, 0]) + + long_line = ParametricFunction(lambda t : (-1, np.pi * t, 0)) + diameter = ParametricFunction(lambda t : (0, t, 0)) + circle = Circle() + for mobject in [circle, diameter, one, long_line, pi_copy]: + mobject.shift([-1, 0, 0]) + mobject.highlight(PI_COLOR) + + return Transform( + CompoundMobject(e, pi, i, equals_neg1), + CompoundMobject(colored_e, colored_pi, colored_i, equals_neg1) + ).then( + Transform( + colored_e, e_digits + ).while_also( + Transform( + colored_i, sqrt_neg1 + ) + ).while_also( + Transform( + colored_pi, pi_copy + ) + ).while_also( + Transform( + circle, long_line, + ) + ).with_background( + diameter, one, colored_e, colored_pi, colored_i, equals_neg1 + ) + ) + +def literal_epii(): + e, pi, i, e_by_e_pi_i_times, what = [ + ImageMobject(name_to_image[name]) + for name in [ + "e", "pi", "i", "e_by_e_pi_i_times", "what" + ] + ] + what.shift([0, -1, 0]) + return Transform( + CompoundMobject(e, pi, i).center().shift((0, 2, 0)), + e_by_e_pi_i_times + ).then( + Animation(what, run_time = 2.1).with_background( + e_by_e_pi_i_times + ) + ) + +def pile_of_equations(): + return ShowCreation( + CompoundMobject( + ImageMobject(name_to_image["pile_of_equations"]).center(), + ImageMobject( + name_to_image["just_trust_it"] + ).center().shift([2, -3, 0]).highlight("red") + ), + alpha_func = None, + run_time = 5.0 + ) + +def confusion_of_terms(): + e, pi, i, e_question, pi_question, i_question, what_it_means = [ + ImageMobject(name_to_image[name]) + for name in [ + "e", "pi", "i", "e_question", "pi_question", "i_question", "what_it_means" + ] + ] + colored_e = deepcopy(e).highlight(E_COLOR) + colored_pi = deepcopy(pi).highlight(PI_COLOR) + colored_i = deepcopy(i).highlight(I_COLOR) + + e_question.highlight(E_COLOR).shift([2, 1, 0]) + pi_question.highlight(PI_COLOR).shift([0, -1, 0]) + i_question.highlight(I_COLOR).shift([-1, 0, 0]) + what_it_means.center().shift((-3, 0, 0)) + + stars = Stars() + + e_anim = Transform(colored_e, e_question) + e_anim.with_background(colored_pi, colored_i) + i_anim = Transform(colored_i, i_question) + i_anim.with_background(e_question, colored_pi) + pi_anim = Transform(colored_pi, pi_question) + pi_anim.with_background(e_question, i_question) + all_questions = CompoundMobject(e_question, pi_question, i_question) + to_stars = Transform( + all_questions, + stars, run_time = 5.0, dither_time = 0 + ) + stall_1 = Animation(all_questions) + to_goal = Transform(stars, what_it_means, dither_time = 0, run_time = 2.0) + for anim in [e_anim, i_anim, pi_anim]: + anim.set_run_time(1.0) + anim.set_dither(0) + for anim in [i_anim, pi_anim, stall_1, to_stars, to_goal]: + e_anim.then(anim) + return e_anim + +def list_of_goals(): + goals = [ + ImageMobject(name_to_image[name]).center() + for name in [ + "what_it_means", "why_it_works", "why_it_is_intuitive" + ] + ] + for x in range(3): + goals[x].shift((3*(x - 1), 0, 0)) + stars = Stars() + rotating_stars = Rotating(stars, radians = np.pi / 3) + return Transform(stars, goals[0]).while_also( + rotating_stars, display = False + ).then( + Reveal(goals[1], dither_time = 0).with_background(goals[0]) + ).then( + Reveal(goals[2]).with_background(*goals[:2]) + ) + +def not_repeated_multiplication(): + e_to_x, e_by_e_x_times, not_what_is_happening = [ + ImageMobject(name_to_image[name]).center() + for name in ["e_to_x", "e_by_e_x_times", "not_what_is_happening"] + ] + not_what_is_happening.rotate(np.pi/7).highlight("red") + return Transform(e_to_x, e_by_e_x_times).then( + Reveal(not_what_is_happening).with_background(e_by_e_x_times) + ).then( + Animation( + CompoundMobject(e_by_e_x_times, not_what_is_happening), + run_time = 1.0 + ) + ) + +def problems_with_repeated_multiplication(): + e, e_by_e, e_by_e_by_e, e_def, how_do_we_define_this = [ + ImageMobject(name_to_image[name]).center() + for name in ["e", "e_by_e", "e_by_e_by_e", + "e_def", "how_do_we_define_this"] + ] + how_do_we_define_this.highlight(E_COLOR).shift((0, -1, 0)) + kwargs = {"run_time" : 0.2, "dither_time" : 0.5} + return Transform(e, e_by_e, **kwargs).then( + Transform(e_by_e, e_by_e_by_e, **kwargs) + ).then( + Transform(e_by_e_by_e, e_def).while_also( + Reveal(how_do_we_define_this) + ) + ) + +def numbers_as_actions(): + three = ImageMobject(name_to_image["three"]).center() + sphere = Sphere().scale(2).highlight(MULTIPLIER_COLOR) + def custom_alpha(t): + return high_inflection_0_to_1(1.7 * t * (1 - t)) + return Transform( + three, sphere, run_time = 3.0, + alpha_func = custom_alpha + ).while_also( + Rotating(sphere, radians = 10 * np.pi), + display = False + ) + + +def numbers_as_counting(): + three = ImageMobject(name_to_image["three"]) + three.center() + spheres = CompoundMobject(*[ + Sphere().scale(0.5).shift([x - 1, 0, 0]) + for x in range(3) + ]) + return Transform( + three, spheres, + ).while_also( + Rotating(spheres, axis = [1, 0, 0]), + display = False + ) + + +def addition_by_counting(): + two, plus, three, five = [ + ImageMobject(name_to_image[name]) + for name in [ + "two", "plus", "three", "five" + ] + ] + tps_center = CompoundMobject(two, plus, three).get_center() + for mob in two, plus, three: + mob.shift(tps_center) + five.center() + spheres = [] + for x in range(5): + spheres.append(Sphere().scale(0.5).shift([x-2, 0, 0])) + first_two = CompoundMobject(*spheres[:2]) + last_three = CompoundMobject(*spheres[2:]) + all_five = CompoundMobject(*spheres) + plus_three = CompoundMobject(plus, three) + return Animation( + CompoundMobject(two, plus_three), + run_time = 2.0, dither_time = 0.0, + ).then( + Transform( + two, first_two, + ).set_dither(0).with_background( + plus_three + ) + ).then( + Transform( + plus_three, last_three, + ).with_background(first_two).set_dither(0) + ).then( + Transform( + all_five, five, + ).set_dither(0) + ).then( + Animation(five, run_time = 1.0, dither_time = 0.0) + ) + +def multiplication_by_counting(): + four, times, five, fours_underbrace, twenty = [ + ImageMobject(name_to_image[name]) + for name in [ + "four", "times", "five", "fours_underbrace", "twenty" + ] + ] + spheres = CompoundMobject(*[ + Sphere().scale(0.5).shift([1.5*x-3, 1.5-y, 0]) + for y in range(4) + for x in range(5) + ]) + fours_underbrace.center() + twenty.center() + rot_spheres = Rotating(spheres, axis = [1, 0, 0], radians = np.pi) + four_times_five = CompoundMobject(four, times, five).center() + first = Transform(four_times_five, fours_underbrace).set_dither(0.5) + second = Transform(copy.deepcopy(fours_underbrace), spheres).set_dither(0) + second.while_also(rot_spheres, display = False).generate_frames() + third = Transform(spheres, twenty).set_dither(0.5) + third.while_also(rot_spheres, display = False) + return first.then(second).then(third) + +def fraction_counting(): + three_point_five = ImageMobject(name_to_image["three_point_five"]).center() + spheres = [ + Sphere().scale(0.5).shift([x-1.5, 0, 0]) + for x in range(4) + ] + num_points = spheres[-1].get_num_points() + for attr in ['points', 'rgbs']: + setattr( + spheres[-1], attr, + getattr(spheres[-1], attr)[:num_points/2, :] + ) + spheres = CompoundMobject(*spheres) + spheres.rotate(np.pi/4, [1, 0, 0]) + return Transform( + three_point_five, spheres, + ).while_also( + Rotating(spheres, axis = [1, 0, 0], radians = np.pi), + display = False + ) + +def irrational_counting(): + sqrt_two = ImageMobject(name_to_image["sqrt_two"]).center() + ish = ImageMobject(name_to_image["ish"]).center() + spheres = [ + Sphere().scale(0.5).rotate(np.pi/4, [1, 0, 0]).shift([x-0.5, 0, 0]) + for x in range(2) + ] + num_points = spheres[-1].get_num_points() + for attr in ['points', 'rgbs']: + setattr( + spheres[-1], attr, + getattr(spheres[-1], attr)[:int(num_points*(np.sqrt(2)-2)), :] + ) + ish.shift([1.3, 0, 0]) + spheres = CompoundMobject(ish, *spheres) + return Transform( + sqrt_two, spheres + ).while_also( + Rotating(spheres, axis = [1, 0, 0], radians = np.pi), + display = False + ) + +def imaginary_counting(): + sqrt_neg1, question_marks = [ + ImageMobject(name_to_image[name]).center() + for name in ["sqrt_neg1", "question_marks"] + ] + return Transform(sqrt_neg1, question_marks) + +def real_number_as_three_things(): + three, adder, multiplier = [ + ImageMobject(name_to_image[name]).center() + for name in ["three", "adder", "multiplier"] + ] + adder_three = copy.deepcopy(three).highlight(ADDER_COLOR) + mult_three = copy.deepcopy(three).highlight(MULTIPLIER_COLOR) + + three.shift([0, 2, 0]) + adder_three.shift([-1, 2, 0]) + mult_three.shift([1, 2, 0]) + + adder.highlight(ADDER_COLOR).shift((2, 2, 0)) + multiplier.highlight(MULTIPLIER_COLOR).shift((2, 2, 0)) + + radius = 2 * SPACE_WIDTH + marked_number_line = NumberLine( + radius = radius, + with_numbers = True + ) + number_line = NumberLine(radius = radius) + three_dist = 3 * number_line.interval_size + + split_three = Transform( + copy.deepcopy(three), + CompoundMobject(three, adder_three, mult_three) + ) + adder_three.center().shift(three.get_center()) + mult_three.center().shift(three.get_center()) + draw_number_line = ShowCreation(marked_number_line) + three_to_point = Transform( + copy.deepcopy(three), + Point((three_dist, 0, 0)) + ) + add_by_three = ApplyFunction( + lambda (x, y, z) : (three_dist+x, y, z), + number_line, dither_time = 2.0 + ).while_also(Reveal(adder, dither_time = 2.0)) + multiply_by_three = ApplyFunction( + lambda (x, y, z) : (3*x, y, z), + number_line, dither_time = 2.0 + ).while_also(Reveal(multiplier, dither_time = 2.0)) + draw_number_line.set_dither(0) + + return split_three.then( + draw_number_line.with_background(three) + ).then( + add_by_three.with_background(adder_three) + ).then( + multiply_by_three.with_background(mult_three) + ) + + +def wrong_adder_conception(): + vert_disp = -0.3 + number_line = NumberLine(radius = SPACE_WIDTH + 3) + nlis = number_line.interval_size + three = ImageMobject(name_to_image["three"]) + three.center().shift((0, 2, 0)).highlight(ADDER_COLOR) + initial_numbers = [ + ImageMobject( + NAME_TO_IMAGE_FILE[str(x)] + ).center().scale(0.5).shift((x*nlis, vert_disp, 0)) + for x in range(-3, 4) + ] + final_numbers = [ + ImageMobject( + NAME_TO_IMAGE_FILE[str(x)] + ).center().scale(0.5).shift((x*nlis, vert_disp, 0)) + for x in range(0, 7) + ] + dots1 = ImageMobject(NAME_TO_IMAGE_FILE["cdots"]).center() + dots2 = copy.deepcopy(dots1).shift((-4 * nlis, vert_disp, 0)) + dots1.shift((4*nlis, vert_disp, 0)) + number_line.add(dots1, dots2) + kwargs = {"run_time" : 2.0, "dither_time" : 1.0} + anim = ComplexFunction(lambda z : z + 3*nlis, number_line, **kwargs) + for init, final in zip(initial_numbers, final_numbers): + anim.while_also(Transform(init, final, **kwargs)) + anim.while_also( + Transform(three, CompoundMobject(*final_numbers), **kwargs) + ) + return anim + +def real_addition_rule(): + three = ImageMobject(name_to_image["three"]).center() + three.shift([0, 2, 0]).highlight(ADDER_COLOR) + marked_number_line = NumberLine(radius = 2 * SPACE_WIDTH, with_numbers = True) + number_line = NumberLine(radius = 2 * SPACE_WIDTH) + three_dist = 3 * number_line.interval_size + shifted_line = copy.deepcopy(number_line).shift((three_dist, 0, 0)) + zero_arrow = Arrow().nudge().shift((0, -0.3, 0)) + three_arrow = Arrow((three_dist, -0.3, 0)).nudge().highlight(ADDER_COLOR) + return Reveal( + CompoundMobject(zero_arrow, three_arrow) + ).with_background(marked_number_line, three).then( + Transform( + CompoundMobject(number_line, zero_arrow), + CompoundMobject(shifted_line, three_arrow), + dither_time = 0, run_time = 3.0 + ).with_background(three, three_arrow) + ).then( + Flash(three_arrow).with_background(shifted_line, three) + ).then( + Animation(CompoundMobject(three_arrow, shifted_line, three)) + ) + +def real_multiplication_rule(): + three = ImageMobject(name_to_image["three"]).center() + three.shift([0, 2, 0]).highlight(MULTIPLIER_COLOR) + marked_number_line = NumberLine(with_numbers = True) + number_line = NumberLine() + three_dist = 3 * number_line.interval_size + stretched_line = copy.deepcopy(number_line) + stretched_line.points = np.array( + map(lambda (x, y, z) : (3*x, y, z), stretched_line.points) + ) + zero_arrow = Arrow(direction = (0, 1, 0)).nudge().shift((0, -0.3, 0)) + zero_arrow.highlight("white") + one_arrow = Arrow().nudge().shift((number_line.interval_size, -0.3, 0)) + one_arrow.highlight(ONE_COLOR) + three_arrow = Arrow((three_dist, -0.3, 0)).nudge().highlight(MULTIPLIER_COLOR) + return Reveal( + CompoundMobject(zero_arrow, one_arrow, three_arrow) + ).with_background(marked_number_line, three).then( + Transform( + CompoundMobject(number_line, one_arrow), + CompoundMobject(stretched_line, three_arrow), + dither_time = 0, run_time = 3.0 + ).with_background(three, three_arrow, zero_arrow) + ).then( + Flash(three_arrow).with_background(stretched_line, three, zero_arrow) + ).then( + Animation(CompoundMobject(zero_arrow, three_arrow, stretched_line, three)) + ) + +def real_addition_by_sliding(): + two, plus, three, five = [ + ImageMobject( + name_to_image[name] + ) + for name in [ + "two", "plus", "three", "five" + ] + ] + center = CompoundMobject(two, plus, three).get_center() + for mob in two, plus, three: + mob.shift(-center + (0, 2, 0)).highlight(ADDER_COLOR) + five.center().shift((0, 2, 0)).highlight(ADDER_COLOR) + number_line = NumberLine(radius = 2 * SPACE_WIDTH) #Numbers? + int_size = number_line.interval_size + shifted_line = dict() + for x in [2, 5]: + shifted_line[x] = copy.deepcopy(number_line).shift((x*int_size, 0, 0)) + return Transform( + copy.deepcopy(number_line), shifted_line[2], dither_time = 0.5 + ).with_background(two).then( + Transform( + shifted_line[2], shifted_line[5], dither_time = 0.5 + ).with_background(two, plus, three) + ).then( + Transform( + number_line, shifted_line[5], dither_time = 0.5 + ).with_background(five) + ) + +def real_multiplication_by_stretching(): + two, times, three, six = [ + ImageMobject( + name_to_image[name] + ).highlight(MULTIPLIER_COLOR) + for name in [ + "two", "times", "three", "six" + ] + ] + center = CompoundMobject(two, times, three).get_center() + for mob in two, times, three: + mob.shift(-center + (0, 2, 0)) + six.center().shift((0, 2, 0)) + number_line = NumberLine() #Numbers? + int_size = number_line.interval_size + stretched_line = dict() + for num in [2, 6]: + stretched_line[num] = copy.deepcopy(number_line) + stretched_line[num].points = np.array( + map(lambda (x, y, z) : (num*x, y, z), stretched_line[num].points) + ) + return Transform( + copy.deepcopy(number_line), stretched_line[2], dither_time = 0.5 + ).with_background(two).then( + Transform( + stretched_line[2], stretched_line[6], dither_time = 0.5 + ).with_background(two, times, three) + ).then( + Transform( + number_line, stretched_line[6], dither_time = 0.5 + ).with_background(six) + ) + + + +def exp_turns_adder_to_muliplier(): + two, e_to_x, e_to_2_value = [ + ImageMobject(name_to_image[name]).center() + for name in ["two", "e_to_x", "e_to_2_value"] + ] + two.center().shift((-2, 2, 0)).highlight(ADDER_COLOR) + e_to_x.center().shift((0, 2, 0)) + e_to_2_value.center().shift((2, 2, 0)).highlight(MULTIPLIER_COLOR) + number_line = NumberLine(radius = SPACE_WIDTH*2) + point = Point(e_to_x.get_center()) + shift_line = ApplyFunction( + lambda (x,y,z) : (x+2*number_line.interval_size,y,z), + copy.deepcopy(number_line), + run_time = 1.0, dither_time = 0 + ) + stretch_line = ApplyFunction( + lambda (x,y,z) : (x*np.exp(2),y,z), + number_line, + run_time = 1.0 + ) + return Reveal(two).set_dither(0).with_background(e_to_x).then( + shift_line.with_background(two, e_to_x) + ).then( + Transform(two, point, dither_time = 0).with_background(e_to_x) + ).then( + Transform(point, e_to_2_value, dither_time = 0).with_background(e_to_x) + ).then( + stretch_line.with_background(e_to_x, e_to_2_value) + ) + +def exp_is_homomorphism(): + two, three, five, e_to_x, e_to_2_value, e_to_3_value, e_to_5_value, plus, times = [ + ImageMobject(name_to_image[name]).center() + for name in ["two", "three", "five", "e_to_x", "e_to_2_value", + "e_to_3_value", "e_to_5_value", "plus", "times"] + ] + two.shift((-2.2, 2.2, 0)) + three.shift((-1.8, 1.8, 0)) + two_three = CompoundMobject(two, three).highlight(ADDER_COLOR) + five.shift((-2, -2, 0)).highlight(ADDER_COLOR) + e_to_2_value.shift((1.8, 2.2, 0)) + e_to_3_value.shift((2.2, 1.8, 0)) + e_to_2_value_e_to_3_value = CompoundMobject(e_to_2_value, e_to_3_value).highlight(MULTIPLIER_COLOR) + e_to_5_value.shift((2, -2, 0)).highlight(MULTIPLIER_COLOR) + e_to_x_copy = copy.deepcopy(e_to_x) + e_to_x.shift((0, 2, 0)) + e_to_x_copy.shift((0, -2, 0)) + plus.shift((-2, 0, 0)) + times.shift((2, 0, 0)) + operations = CompoundMobject(e_to_x, e_to_x_copy, plus, times) + high_exp_point = Point(e_to_x.get_center()) + low_exp_point = Point(e_to_x_copy.get_center()) + plus_point = Point(plus.get_center()) + times_point = Point(times.get_center()) + + anim = Transform( + two_three, plus_point, dither_time = 0 + ).with_background(operations) + for start, end in [ + (plus_point, five), + (five, low_exp_point), + (low_exp_point, e_to_5_value), + (copy.deepcopy(two_three), high_exp_point), + (high_exp_point, e_to_2_value_e_to_3_value), + (e_to_2_value_e_to_3_value, times_point), + (times_point, e_to_5_value) + ]: + anim.then(Transform(start, end, dither_time = 0), + carry_over_background = True + ) + if end in [e_to_5_value, five, e_to_2_value_e_to_3_value]: + anim.then( + Animation(end, run_time = 1.0, dither_time = 0), + carry_over_background = True, + ) + return anim + +def repeated_product_gives_property(): + expressions = [ + ImageMobject(name_to_image[name]).center() + for name in ["e_to_x_plus_y", "e_by_e_x_plus_y_times", + "x_es_then_y_es", "e_to_x_e_to_y",] + ] + anim = Transform(expressions[0], expressions[1], run_time = 0.5) + for x in range(1, 3): + anim.then( + Transform(expressions[x], expressions[x + 1]), + run_time = 0.5 + ) + return anim + + +def repeated_product_as_consequence(): + defining_property, other_way_around, e_to_5, \ + e_to_sum_ones, e_to_1_product = [ + ImageMobject(name_to_image[name]).center() + for name in ["defining_property", "other_way_around", "e_to_5", + "e_to_sum_ones", "e_to_1_product"] + ] + return Animation(other_way_around, run_time = 1.0, dither_time = 0).then( + Animation(defining_property, run_time = 2.0, dither_time = 0) + ).then( + Transform(e_to_5, e_to_sum_ones, run_time = 0.5) + ).then( + Transform(e_to_sum_ones, e_to_1_product, run_time = 0.5) + ) + + +def e_to_x_definition(): + adder_to_multiplier_property, e_to_x_series, \ + colon_explanation, e_def, e_approx, e_in_nature = [ + ImageMobject(name_to_image[name]) + for name in [ + "adder_to_multiplier_property", + "e_to_x_series", + "colon_explanation", + "e_def", + "e_approx", + "e_in_nature" + ] + ] + for mob in e_to_x_series, colon_explanation: + mob.center() + colon_explanation.shift((-1, 1.5, 0)) + colon_explanation.add(Arrow(point = (-3.5, 0.4, 0), direction = (-1,-1,0))) + e_in_nature.shift((0, -2, 0)) + for mob in e_def, e_approx: + mob.shift((0, 1, 0)) + return Transform( + adder_to_multiplier_property, + e_to_x_series + ).then( + Reveal(colon_explanation).with_background(e_to_x_series) + ).then( + Transform(e_to_x_series, e_def).while_also( + Reveal(CompoundMobject(e_approx, e_in_nature)) + ) + ) + +def less_natural_exponentials(): + two, five, i, to_the_x, adder_to_multiplier_property = [ + ImageMobject(name_to_image[name]) + for name in ["two", "five", "i", "to_the_x", + "adder_to_multiplier_property"] + ] + two.center() + five.center() + i.scale(1.5).center() + to_the_x.shift((0, -0.25, 0)) + kwargs = {"dither_time" : 0.5} + return Transform( + adder_to_multiplier_property, + CompoundMobject(two, to_the_x), **kwargs + ).then( + Transform(two, five, **kwargs).with_background( + to_the_x + ) + ).then( + Transform(five, i, **kwargs).with_background( + to_the_x + ) + ) + + +def complex_addition(num): + complex_plane = Grid(radius = 2 * SPACE_WIDTH).add(Cross()) + point = Cross().shift((num.real, num.imag, 0)).highlight(ADDER_COLOR) + return ShowCreation(point).with_background(complex_plane).then( + ComplexFunction( + (lambda z : z + num), complex_plane + ).with_background( + point + ) + ) + +def complex_multiplication(num): + complex_plane = Grid(radius = 2 * SPACE_WIDTH) + zero = Cross() + one = Cross().shift((1, 0, 0)).highlight(ONE_COLOR) + num_dot = Cross().shift((num.real, num.imag, 0)).highlight(MULTIPLIER_COLOR) + return ComplexFunction( + (lambda z : z*num), complex_plane + ).while_also( + ComplexFunction((lambda z : z + num - 1), one) + ).with_background( + zero, num_dot + ) + +def multiply_i_twice(): + zero, one, i, new_one = [ + Cross(), + Cross().shift((1, 0, 0)).highlight(ONE_COLOR), + Cross().shift((0, 1, 0)).highlight(MULTIPLIER_COLOR), + Cross().shift((0, 1, 0)).highlight(ONE_COLOR), + ] + complex_plane = Grid(radius = 2 * SPACE_WIDTH) + return RotationAsTransform(complex_plane, np.pi/2).while_also( + RotationAsTransform(one, np.pi/2) + ).with_background(zero, i).then( + RotationAsTransform(complex_plane, np.pi/2).while_also( + RotationAsTransform(CompoundMobject(new_one), np.pi/2) + ).with_background(zero) + ) + +def multiply_neg_1(): + zero, one, neg_1 = [ + Cross(), + Cross().shift((1, 0, 0)).highlight(ONE_COLOR), + Cross().shift((-1, 0, 0)).highlight(MULTIPLIER_COLOR), + ] + complex_plane = Grid(radius = 2 * SPACE_WIDTH).add(zero, one) + return RotationAsTransform( + complex_plane.add(zero, one), np.pi + ).with_background(neg_1) + +def i_squared_equals_neg_1(): + equation = ImageMobject(name_to_image["i_squared_equals_neg_1"]).center() + plane = Grid().add(Cross(), Cross().shift((-1, 0, 0)).highlight(MULTIPLIER_COLOR)) + return Transform(plane, equation, dither_time = 2.0) + +def reals_in_complex(): + radius = 2 * SPACE_WIDTH + plain_complex_plane = Grid(radius = radius) + complex_plane = copy.deepcopy(plain_complex_plane) + complex_plane.highlight(NumberLine.DEFAULT_COLOR, lambda (x, y, z) : y == 0) + shifted = copy.deepcopy(complex_plane).shift((3, 0, 0)) + stretched = copy.deepcopy(complex_plane).scale(2) + anim = Transform(plain_complex_plane, complex_plane) + for start, end in [ + (copy.deepcopy(complex_plane), shifted), + (shifted, complex_plane), + (copy.deepcopy(complex_plane), stretched), + (stretched, complex_plane) + ]: + anim.then(Transform(start, end, run_time = 2.0, dither_time = 0.0)) + return anim + +def broken_up_complex_addition(num): + complex_plane = Grid(radius = 2 * SPACE_WIDTH).add(Cross()) + point = Cross().shift((num.real, num.imag, 0)).highlight(ADDER_COLOR) + plane_plus_real = copy.deepcopy(complex_plane).shift((num.real, 0, 0)) + plane_plus_num = copy.deepcopy(complex_plane).shift((num.real, num.imag, 0)) + return Transform(complex_plane, plane_plus_real).with_background(point).then( + Transform(plane_plus_real, plane_plus_num), + carry_over_background = True + ) + +def broken_up_complex_multiplication(num): + complex_plane = Grid(radius = 2 * SPACE_WIDTH) + zero = Cross() + one = Cross().shift((1, 0, 0)).highlight(ONE_COLOR) + num_dot = Cross().shift((num.real, num.imag, 0)).highlight(MULTIPLIER_COLOR) + plane_stretched = copy.deepcopy(complex_plane).scale(np.linalg.norm(num)) + plane_times_num = copy.deepcopy(plane_stretched).rotate(np.log(num).imag) + one_stretched = copy.deepcopy(one).shift((np.linalg.norm(num) - 1, 0, 0)) + one_times_num = copy.deepcopy(one).center().shift(num_dot.get_center()) + return Transform( + complex_plane, plane_stretched + ).with_background(zero, num_dot).while_also( + Transform(one, one_stretched) + ).then( + Transform( + plane_stretched, plane_times_num + ).while_also(Transform(one_stretched, one_times_num)), + carry_over_background = True + ) + +def new_dimensions(): + adder = ComplexFunction(lambda z : z + complex(0, 1)) + multiplier = RotationAsTransform(Grid(radius = SPACE_HEIGHT + SPACE_WIDTH), np.pi / 3) + for anim in adder, multiplier: + anim.set_run_time(5.0).set_dither(0.5).set_alpha_func(there_and_back) + return adder.then(multiplier) + + +def wrap_imaginaries_to_circle(): + imaginaries = ParametricFunction( + lambda t : (0, t * SPACE_HEIGHT, 0), + color = ADDER_COLOR + ) + imaginaries.shift((0.01, 0, 0)) + circle = Circle(color = MULTIPLIER_COLOR) + Mobject.align_data(imaginaries, circle) + complex_plane = Grid() + hidden_plane = Grid(color = "black") + return FadeOut( + complex_plane, alpha_func = there_and_back + ).with_background(imaginaries).then( + ShowCreation(circle).with_background(imaginaries, complex_plane), + ).then( + ComplexFunction(np.exp, imaginaries).with_background( + circle, complex_plane), + ) + +def wrap_imaginaries_to_circle_with_measurments(): + pi, i, minus, neg_1 = [ + ImageMobject(name_to_image[name]) + for name in ["pi", "i", "minus", "neg_1"] + ] + pi_i = CompoundMobject(pi, i).center() + minus_pi_i = copy.deepcopy(pi_i).center().add( + minus.center().scale(0.5).shift((-0.2, 0, 0)) + ) + for mob, sgn in (pi_i, 1), (minus_pi_i, -1): + mob.shift((-0.3, sgn*np.pi, 0)) + mob.add( + Cross().scale(0.5).shift((0, sgn*np.pi, 0)).highlight(ADDER_COLOR) + ) + neg_1.center().shift((-1.2, 0, 0)) + imaginaries = ParametricFunction( + lambda t : (0, np.pi * t, 0), + color = ADDER_COLOR + ) + complex_plane = Grid() + circle = Circle(color = MULTIPLIER_COLOR) + return ComplexFunction(np.exp, imaginaries).with_background( + circle, complex_plane + ).while_also( + Transform(CompoundMobject(pi_i, minus_pi_i), neg_1, + run_time = DEFAULT_ANIMATION_RUN_TIME, + ) + ) + +def definition_of_pi(): + pi, one, two = [ + ImageMobject(name_to_image[name]).center() + for name in [ + "pi", "one", "two" + ] + ] + two.scale(0.7).shift((-0.1, 0, 0)) + two_pi = CompoundMobject(two, pi.shift((0.1, 0, 0))) + one.shift((0.5, 0.3, 0)) + two_pi.shift((0, -1.3, 0)) + circle = Circle().rotate(np.pi/4) + line = Line((-np.pi, -1, 0), (np.pi, -1, 0)).highlight(Circle.DEFAULT_COLOR) + radius = Line((0, 0, 0), (1, 0, 0)) + kwargs = {"run_time" : 2, "dither_time" : 1} + return Transform(circle, line, **kwargs).with_background( + one, radius + ).while_also( + Reveal(two_pi, **kwargs) + ) + + +def epii_adder_to_multiplier(): + e, pi, i, equals_neg1, neg_1 = [ + ImageMobject(name_to_image[name]) + for name in [ + "e", "pi", "i", "equals_neg1", "neg_1" + ] + ] + pi.highlight(ADDER_COLOR) + i.highlight(ADDER_COLOR) + equals_neg1.highlight(MULTIPLIER_COLOR, lambda (x, y, z) : x > 0.25) + neg_1.highlight(MULTIPLIER_COLOR) + epii_neg1 = CompoundMobject(e, pi, i, equals_neg1) + + dividing_line = Line((0, -SPACE_HEIGHT, 0), (0, SPACE_HEIGHT, 0)) + half_width = SPACE_WIDTH / 2 + left_grid = Grid(radius = SPACE_HEIGHT + np.pi).filter_out( + lambda (x, y, z) : abs(x) > half_width + ).add(Cross()) + right_grid = Grid().add(Cross(), Cross().highlight(ONE_COLOR).shift((1, 0, 0))) + # pi_i = CompoundMobject(pi, i).center().shift((-0.25, np.pi, 0)) + pi_i = Cross().highlight(ADDER_COLOR).shift((0, np.pi, 0)) + # neg_1.center().shift((-1.5, 0, 0)) + neg_1 = Cross().highlight(MULTIPLIER_COLOR).shift((-1, 0, 0)) + return Animation(epii_neg1, run_time = 2, dither_time = 0).then( + ComplexFunction(lambda z : z + complex(0, np.pi), left_grid).with_background( + pi_i, dividing_line.shift((half_width, 0, 0)) + ).shift((-half_width, 0, 0)).while_also( + RotationAsTransform(right_grid, np.pi).with_background( + neg_1 + ).restrict_width(half_width).shift((half_width, 0, 0)) + ).while_also( + Animation(epii_neg1) + ) + ) + + # return Animation(epii_neg1, run_time = 1.0, dither_time = 0).then( + # ComplexFunction(lambda z : z + complex(0, np.pi), Grid(radius = 2*SPACE_HEIGHT)) + # ).with_background(epii_neg1).then( + # RotationAsTransform(Grid(radius = 2*SPACE_HEIGHT), np.pi) + # ).with_background(epii_neg1) + +def e_to_all_kinds_of_things(): + expressions = [ + ImageMobject(name_to_image[name]) + for name in [ + "e_to_complex", "e_to_matrix", "e_to_derivative", "e_def" + ] + ] + return reduce( + Animation.then, + [ + Transform(expressions[x], expressions[x + 1]) + for x in range(3) + ] + ) + + + +if __name__ == '__main__': + example_complex = complex(2, 1) + functions = [ + # (logo_to_epii, []), + # (write_epii, []), + # (the_terms, []), + # (literal_epii, []), + # (pile_of_equations, []), + # (confusion_of_terms, []), + # (list_of_goals, []), + # (not_repeated_multiplication, []), + # (problems_with_repeated_multiplication, []), + # (numbers_as_actions, []), + # (numbers_as_counting, []), + # (addition_by_counting, []), + # (multiplication_by_counting, []), + # (fraction_counting, []), + # (irrational_counting, []), + # (imaginary_counting, []), + # (real_number_as_three_things, []), + # (wrong_adder_conception, []), + # (real_addition_rule, []), + # (real_multiplication_rule, []), + # (real_addition_by_sliding, []), + # (real_multiplication_by_stretching, []), + # (exp_turns_adder_to_muliplier, []), + # (exp_is_homomorphism, []), + # (repeated_product_gives_property, []), + # (repeated_product_as_consequence, []), + (e_to_x_definition, []), + # (less_natural_exponentials, []), + # (complex_addition, [example_complex]), + # (complex_multiplication, [example_complex]), + # (multiply_i_twice, []), + # (multiply_neg_1, []), + # (i_squared_equals_neg_1, []), + # (complex_addition, [complex(0, 1)]), + # (reals_in_complex, []), + # (broken_up_complex_addition, [example_complex]), + # (broken_up_complex_multiplication, [example_complex]), + # (new_dimensions, []), + # (wrap_imaginaries_to_circle, []), + # (wrap_imaginaries_to_circle_with_measurments, []), + # (definition_of_pi, []), + # (epii_adder_to_multiplier, []), + # (e_to_all_kinds_of_things, []), + ] + + full_path = os.path.join(MOVIE_DIR, EPII_MOVIE_DIR) + if not os.path.exists(full_path): + os.mkdir(full_path) + for func, args in functions: + name = os.path.join( + EPII_MOVIE_DIR, + to_cammel_case(func.__name__) + hash_args(args) + ) + func(*args).write_to_movie(name) + + for anim in [ + # ShowCreation(Grid(), run_time = 3.0), + # Rotating(Stars(), radians = np.pi / 3), + # ComplexFunction(np.exp, Grid(radius = SPACE_HEIGHT)) + ]: + anim.write_to_movie(os.path.join(EPII_MOVIE_DIR, str(anim))) + + + + + + + + + diff --git a/epii/poem_animations.py b/epii/poem_animations.py new file mode 100644 index 00000000..d9c8d6ed --- /dev/null +++ b/epii/poem_animations.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python + +from PIL import Image +from animate import * +from mobject import * +from constants import * +from helpers import * +from tex_image_utils import load_pdf_images +from displayer import * +import itertools as it +import os +import numpy as np +from copy import deepcopy + +from epii_animations import name_to_image + + +PI_COLOR = "red" +E_COLOR = "skyblue" +I_COLOR = "green" +ADDER_COLOR = "limegreen" +MULTIPLIER_COLOR = "yellow" +ONE_COLOR = "skyblue" + +POEM_MOVIE_DIR = "poem" + +symbol_images = load_pdf_images("epii_poem.pdf", regen_if_exists = False) + +RUN_TIMES = [ + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, + 0.4, +] +DITHER_TIMES = [ + 0, + 0.1, + 0, + 0.1, + 0, + 0.05, + 0.0, + 0.1, +] +LAST_FRAME_REST_KWARGS = {"run_time" : 1.0, "dither_time" : 0} +LINE_KWARGS = [ + {"run_time" : run_time, "dither_time" : dither_time} + for run_time, dither_time in zip(RUN_TIMES, DITHER_TIMES) +] + +LINES_PER_VERSE = 8 +LINES_PER_LAST_VERSE = 4 +VERSES = 10 + +def get_text_transitions(verse): + num_lines = LINES_PER_LAST_VERSE if (verse == VERSES - 1) else LINES_PER_VERSE + lines = [ + ImageMobject(symbol_images[LINES_PER_VERSE * verse + x]) + for x in range(num_lines) + ] + lines[2].shift((-1, 0, 0)) + transitions = [] + for x in range(num_lines): + if x == 0: + transition = Animation(lines[x], **LINE_KWARGS[x]) + elif x == 1: + transition = Reveal(lines[x], **LINE_KWARGS[x]) + elif x in range(2, num_lines-1): + transition = Transform(lines[x-2], lines[x], **LINE_KWARGS[x]) + else: + transition = Transform( + CompoundMobject(lines[x-2], lines[x-1]), lines[x], + **LINE_KWARGS[x] + ) + if x in range(1, num_lines-1): + transition.with_background(lines[x - 1]) + transitions.append(transition) + return transitions + +def augment_verse_0(transitions): + mobs = [e, pi, i, equals_neg1] = [ + ImageMobject(name_to_image[name]) + for name in ["e", "pi", "i", "equals_neg1"] + ] + center = CompoundMobject(*mobs).get_center() + for mob in mobs: + mob.shift(-center) + for x, mob in zip([1, 2, 3, 7], mobs): + transitions[x].while_also(ShowCreation(mob, **LINE_KWARGS[x])) + for y in range(x + 1, LINES_PER_VERSE): + transitions[y].with_background(mob) + +def augment_verse_1(transitions): + e, pi, i, e_by_e_pi_i_times = [ + ImageMobject(name_to_image[name]) + for name in ["e", "pi", "i", "e_by_e_pi_i_times"] + ] + epii = CompoundMobject(e, pi, i).center() + for x in range(4): + transitions[x].with_background(epii) + transitions[4].while_also(Transform(epii, e_by_e_pi_i_times, **LINE_KWARGS[4])) + for x in range(5, LINES_PER_VERSE): + transitions[x].with_background(e_by_e_pi_i_times) + +def augment_verse_2(transitions): + e, pi, i, pi_question, i_question = [ + ImageMobject(name_to_image[name]) + for name in ["e", "pi", "i", "pi_question", "i_question"] + ] + center = CompoundMobject(e, pi, i).get_center() + for mob in e, pi, i: + mob.shift(-center) + pi.highlight(PI_COLOR) + pi_question.highlight(PI_COLOR).shift((-1, -1, 0)) + i.highlight(I_COLOR) + i_question.highlight(I_COLOR).shift((1, 1, 0)) + for x in [2, 3]: + transitions[x].with_background(pi_question, i_question) + transitions[4].while_also( + Transform(pi_question, pi, **LINE_KWARGS[4]) + ).with_background(i_question) + transitions[5].while_also( + Transform(i_question, i, **LINE_KWARGS[5]) + ).with_background(pi) + for x in [6, 7]: + transitions[x].with_background(pi, i) + transitions[7].while_also(Reveal(e, **LINE_KWARGS[7])) + +def augment_verse_3(transitions): + one, i, minus, two, three_point_five = [ + ImageMobject(name_to_image[name]) + for name in ["one", "i", "minus", "two", "three_point_five"] + ] + minus.shift((-0.8, 0.25, 0)) + minus_two = CompoundMobject(minus, two) + nums = [one, i, minus_two, three_point_five] + for num in nums: + num.center().shift((0.5, 0, 0)).highlight(ADDER_COLOR) + i.scale(2) + + plane = Grid(radius = SPACE_WIDTH + SPACE_HEIGHT) + transitions[3].while_also(ShowCreation(plane, **LINE_KWARGS[3])) + for x, c, num in zip([4, 5, 6, 7], [1, complex(0, 1), -2, 3.5], nums): + transitions[x].while_also( + ComplexFunction(lambda z : z + c, plane, **LINE_KWARGS[x]) + ).with_background(num) + +def augment_verse_4(transitions): + i, two = [ + ImageMobject(name_to_image[name]) + for name in ["i", "two"] + ] + for num in i, two: + num.center().shift((0.5, 0, 0)).highlight(MULTIPLIER_COLOR) + + plane = Grid(radius = SPACE_WIDTH + SPACE_HEIGHT) + for x in [0, 1, 2, 3, 6, 7]: + transitions[x].with_background(plane) + transitions[4].while_also( + RotationAsTransform(plane, np.pi/2, **LINE_KWARGS[4]) + ).with_background(i) + transitions[5].while_also( + ComplexFunction(lambda z : 2*z, plane, **LINE_KWARGS[5]) + ).with_background(two) + +def augment_verse_5(transitions): + e_to_x = ImageMobject(name_to_image["e_to_x"]).center() + for transition in transitions: + transition.with_background(e_to_x) + +def augment_verse_6(transitions): + e_to_x, e_by_e_x_times, not_what_is_happening, two, e_to_2 = [ + ImageMobject(name_to_image[name]) + for name in ["e_to_x", "e_by_e_x_times", "not_what_is_happening", + "two", "e_to_2_value"] + ] + two.center().shift((-2, 0, 0)).highlight(ADDER_COLOR) + e_to_2.center().shift((2, 0, 0)).highlight(MULTIPLIER_COLOR) + e_to_x.center() + point = Point() + not_what_is_happening.rotate(np.pi/7).highlight("red") + transitions[0].while_also(Transform(e_to_x, e_by_e_x_times, **LINE_KWARGS[0])) + transitions[1].while_also( + Reveal(not_what_is_happening, **LINE_KWARGS[1]) + ).with_background(e_by_e_x_times) + for x in [2, 3]: + transitions[x].with_background(e_by_e_x_times, not_what_is_happening) + transitions[4].with_background(two, e_to_x) + transitions[5].while_also( + Transform(two, point, **LINE_KWARGS[5]) + ).with_background(e_to_x) + transitions[6].while_also( + Transform(point, e_to_2, **LINE_KWARGS[6]) + ).with_background(e_to_x) + transitions[7].with_background(e_to_2, e_to_x) + + +def augment_verse_7(transitions): + plane = Grid(SPACE_HEIGHT + SPACE_WIDTH) + for x, c in zip([0, 1, 4, 5], [1, -1, complex(0, 1), complex(0, -1)]): + transitions[x].while_also( + ComplexFunction(lambda z : z + c, plane, **LINE_KWARGS[x]) + ) + big_plane = copy.deepcopy(plane).scale(2) + for x, c, mob in zip([2, 3], [2, 0.5], [plane, big_plane]): + transitions[x].while_also( + ComplexFunction(lambda z : z*c, mob, **LINE_KWARGS[x]) + ) + rotated_plane = copy.deepcopy(plane).rotate(np.pi / 4) + for x, r, mob in zip([6, 7], [np.pi/4, -np.pi/4], [plane, rotated_plane]): + transitions[x].while_also( + RotationAsTransform(mob, r, **LINE_KWARGS[x]) + ) + +def augment_verse_8(transitions): + pi, i, neg_1 = [ + ImageMobject(name_to_image[name]) + for name in ["pi", "i", "neg_1"] + ] + pi_i = CompoundMobject(pi, i).center() + pi_i.shift((-0.3, np.pi, 0)) + neg_1.center().shift((-1.2, 0, 0)) + imaginaries = ParametricFunction( + lambda t : (0, np.pi * t, 0), + color = ADDER_COLOR + ) + plane = Grid() + circle = Circle(color = MULTIPLIER_COLOR) + + transitions[0].with_background(plane) + transitions[1].while_also( + Reveal(pi_i, **LINE_KWARGS[1]) + ).with_background(plane) + transitions[2].while_also( + ComplexFunction(lambda z : z + np.pi * complex(0, 1), **LINE_KWARGS[2]) + ).with_background(pi_i) + transitions[3].with_background(plane, pi_i) + transitions[4].while_also( + Transform(imaginaries, circle, **LINE_KWARGS[4]) + ).while_also( + Transform(pi, neg_1, **LINE_KWARGS[4]) + ).with_background(plane) + transitions[5].with_background(plane, neg_1, circle) + transitions[6].while_also( + RotationAsTransform(plane, np.pi, **LINE_KWARGS[6]) + ).with_background(circle, neg_1) + transitions[7].with_background(plane, circle, neg_1) + +def augment_verse_9(transitions): + mobs = [e, pi, i, equals_neg1] = [ + ImageMobject(name_to_image[name]) + for name in ["e", "pi", "i", "equals_neg1"] + ] + epii_neg1 = CompoundMobject(*mobs).center() + for transition in transitions: + transition.with_background(epii_neg1) + + +if __name__ == '__main__': + augment_verse = [ + augment_verse_0, + augment_verse_1, + augment_verse_2, + augment_verse_3, + augment_verse_4, + augment_verse_5, + augment_verse_6, + augment_verse_7, + augment_verse_8, + augment_verse_9, + ] + for verse in range(VERSES): + transitions = get_text_transitions(verse) + augment_verse[verse](transitions) + name = os.path.join(POEM_MOVIE_DIR, "Verse%d"%verse) + reduce(Animation.then, transitions).write_to_movie(name) + + + + + + + + + + + + + + + + + + diff --git a/generate_logo.py b/generate_logo.py new file mode 100644 index 00000000..ad0ffbd6 --- /dev/null +++ b/generate_logo.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +from PIL import Image +from animate import * +from mobject import * +from constants import * +from helpers import * +import itertools as it +import os + + +import numpy as np + +DARK_BLUE = "#236B8E" +DARK_BROWN = "#8B4513" +LIGHT_BROWN = "#CD853F" + +size = 1.5 +circle = Circle(color = 'skyblue').repeat(4).scale(size) +sphere = Sphere(density = 100, color = DARK_BLUE).scale(size) +sphere.rotate(-np.pi / 7, [1, 0, 0]) +sphere.rotate(-np.pi / 7) +alpha = 0.3 +iris = Mobject() +Mobject.interpolate(circle, sphere, iris, alpha) +for mob, color in [(iris, LIGHT_BROWN), (circle, DARK_BROWN)]: + mob.highlight(color, lambda (x, y, z) : x < 0 and y > 0) + mob.highlight("black", lambda point: np.linalg.norm(point) < size/2) + +name = ImageMobject(NAME_TO_IMAGE_FILE["3Blue1Brown"]).center() +name.highlight("gray") +# name.highlight(DARK_BROWN, lambda (x, y, z) : x < 0 and y > 0) +name.shift((0, -2, 0)) + +create_eye = Transform( + circle, iris, + run_time = DEFAULT_ANIMATION_RUN_TIME, + name = "LogoGeneration" +).then( + Animation(name, dither_time = 0) +).drag_pixels() +create_eye.write_to_movie() +index = int(DEFAULT_ANIMATION_RUN_TIME / DEFAULT_ANIMATION_PAUSE_TIME) +create_eye.frames[index].save(LOGO_PATH) diff --git a/helpers.py b/helpers.py new file mode 100644 index 00000000..ff286c70 --- /dev/null +++ b/helpers.py @@ -0,0 +1,140 @@ +import numpy as np +import itertools as it +from PIL import Image +from colour import Color +from random import random +import string +from constants import * + +def hash_args(args): + args = map(lambda arg : arg.__name__ if type(arg) == type(hash_args) else arg, args) + return str(hash(str(args))%1000) if args else "" + +def random_color(): + color = Color() + color.set_rgb([1 - 0.5 * random() for x in range(3)]) + return color + +def to_cammel_case(name): + parts = name.split("_") + parts = [ + filter( + lambda c : c not in string.punctuation + string.whitespace, part + ).capitalize() + for part in parts + ] + return "".join(parts) + +def drag_pixels(images): + curr = np.array(images[0]) + new_images = [] + for image in images: + curr += (curr == 0) * np.array(image) + new_images.append(Image.fromarray(curr)) + return new_images + +def invert_image(image): + arr = np.array(image) + arr = (255 * np.ones(arr.shape)).astype(arr.dtype) - arr + return Image.fromarray(arr) + +def make_even(iterable_1, iterable_2): + list_1, list_2 = list(iterable_1), list(iterable_2) + length = max(len(list_1), len(list_2)) + return ( + [list_1[(n * len(list_1)) / length] for n in xrange(length)], + [list_2[(n * len(list_2)) / length] for n in xrange(length)] + ) + +def make_even_by_cycling(iterable_1, iterable_2): + length = max(len(iterable_1), len(iterable_2)) + cycle1 = it.cycle(iterable_1) + cycle2 = it.cycle(iterable_2) + return ( + [cycle1.next() for x in range(length)], + [cycle2.next() for x in range(length)] + ) + +def sigmoid(x): + return 1.0/(1 + np.exp(-x)) + +def high_inflection_0_to_1(t, inflection = 10.0): + error = sigmoid(-inflection / 2) + return (sigmoid(inflection*(t - 0.5)) - error) / (1 - 2*error) + +def there_and_back(t, inflection = 10.0): + new_t = 2*t if t < 0.5 else 2*(1 - t) + return high_inflection_0_to_1(new_t, inflection) + + +def composition(func_list): + """ + func_list should contain elements of the form (f, args) + """ + return reduce( + lambda (f1, args1), (f2, args2) : (lambda x : f1(f2(x, *args2), *args1)), + func_list, + lambda x : x + ) + +def remove_nones(sequence): + return filter(lambda x : x, sequence) + +#Matrix operations +def rotation_matrix(angle, axis): + """ + Rotation in R^3 about a specified axess of rotation. + """ + about_z = rotation_about_z(angle) + z_to_axis = z_to_vector(axis) + axis_to_z = np.linalg.inv(z_to_axis) + return reduce(np.dot, [z_to_axis, about_z, axis_to_z]) + +def rotation_about_z(angle): + return [ + [np.cos(angle), -np.sin(angle), 0], + [np.sin(angle), np.cos(angle), 0], + [0, 0, 1] + ] + +def z_to_vector(vector): + """ + Returns some matrix in SO(3) which takes the z-axis to the + (normalized) vector provided as an argument + """ + norm = np.linalg.norm(vector) + if norm == 0: + return np.identity(3) + v = np.array(vector) / norm + phi = np.arccos(v[2]) + if any(v[:2]): + #projection of vector to {x^2 + y^2 = 1} + axis_proj = v[:2] / np.linalg.norm(v[:2]) + theta = np.arccos(axis_proj[0]) + if axis_proj[1] < 0: + theta = -theta + else: + theta = 0 + phi_down = np.array([ + [np.cos(phi), 0, np.sin(phi)], + [0, 1, 0], + [-np.sin(phi), 0, np.cos(phi)] + ]) + return np.dot(rotation_about_z(theta), phi_down) + +def rotate_vector(vector, angle, axis): + #Slightly hacky, changes vector in place + vector[:3] = np.dot(rotation_matrix(angle, axis), vector) + +def angle_between(v1, v2): + return np.arccos(np.dot( + v1 / np.linalg.norm(v1), + v2 / np.linalg.norm(v2) + )) + + + + + + + diff --git a/images2gif.py b/images2gif.py new file mode 100644 index 00000000..ed54366a --- /dev/null +++ b/images2gif.py @@ -0,0 +1,845 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010, Almar Klein, Ant1, Marius van Voorden +# +# This code is subject to the (new) BSD license: +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" Module images2gif + +Provides functionality for reading and writing animated GIF images. +Use writeGif to write a series of numpy arrays or PIL images as an +animated GIF. Use readGif to read an animated gif as a series of numpy +arrays. + +Acknowledgements +---------------- + +Many thanks to Ant1 for: +* noting the use of "palette=PIL.Image.ADAPTIVE", which significantly + improves the results. +* the modifications to save each image with its own palette, or optionally + the global palette (if its the same). + +Many thanks to Marius van Voorden for porting the NeuQuant quantization +algorithm of Anthony Dekker to Python (See the NeuQuant class for its +license). + +This code is based on gifmaker (in the scripts folder of the source +distribution of PIL) + +Some implementation details are ased on gif file structure as provided +by wikipedia. + +""" + +import os +import progressbar + +try: + import PIL + from PIL import Image, ImageChops + from PIL.GifImagePlugin import getheader, getdata +except ImportError: + PIL = None + +try: + import numpy as np +except ImportError: + np = None + +try: + from scipy.spatial import cKDTree +except ImportError: + cKDTree = None + +# getheader gives a 87a header and a color palette (two elements in a list). +# getdata()[0] gives the Image Descriptor up to (including) "LZW min code size". +# getdatas()[1:] is the image data itself in chuncks of 256 bytes (well +# technically the first byte says how many bytes follow, after which that +# amount (max 255) follows). + +def checkImages(images): + """ checkImages(images) + Check numpy images and correct intensity range etc. + The same for all movie formats. + """ + # Init results + images2 = [] + + for im in images: + if PIL and isinstance(im, PIL.Image.Image): + # We assume PIL images are allright + images2.append(im) + + elif np and isinstance(im, np.ndarray): + # Check and convert dtype + if im.dtype == np.uint8: + images2.append(im) # Ok + elif im.dtype in [np.float32, np.float64]: + im = im.copy() + im[im<0] = 0 + im[im>1] = 1 + im *= 255 + images2.append( im.astype(np.uint8) ) + else: + im = im.astype(np.uint8) + images2.append(im) + # Check size + if im.ndim == 2: + pass # ok + elif im.ndim == 3: + if im.shape[2] not in [3,4]: + raise ValueError('This array can not represent an image.') + else: + raise ValueError('This array can not represent an image.') + else: + raise ValueError('Invalid image type: ' + str(type(im))) + + # Done + return images2 + + +def intToBin(i): + """ Integer to two bytes """ + # devide in two parts (bytes) + i1 = i % 256 + i2 = int( i/256) + # make string (little endian) + return chr(i1) + chr(i2) + + +def getheaderAnim(im): + """ Animation header. To replace the getheader()[0] """ + bb = "GIF89a" + bb += intToBin(im.size[0]) + bb += intToBin(im.size[1]) + bb += "\x87\x00\x00" + return bb + + +def getImageDescriptor(im): + """ Used for the local color table properties per image. + Otherwise global color table applies to all frames irrespective of + wether additional colours comes in play that require a redefined palette + Still a maximum of 256 color per frame, obviously. + + Written by Ant1 on 2010-08-22 + """ + bb = '\x2C' # Image separator, + bb += intToBin( 0 ) # Left position + bb += intToBin( 0 ) # Top position + bb += intToBin( im.size[0] ) # image width + bb += intToBin( im.size[1] ) # image height + bb += '\x87' # packed field : local color table flag1, interlace0, sorted table0, reserved00, lct size111=7=2^(7+1)=256. + # LZW minimum size code now comes later, begining of [image data] blocks + return bb + + +#def getAppExt(loops=float('inf')): +#compile error commented by zcwang +def getAppExt(loops=float(0)): + """ Application extention. Part that specifies amount of loops. + If loops is inf, it goes on infinitely. + """ + if loops == 0: + loops = 2**16-1 + #bb = "" # application extension should not be used + # (the extension interprets zero loops + # to mean an infinite number of loops) + # Mmm, does not seem to work + if True: + bb = "\x21\xFF\x0B" # application extension + bb += "NETSCAPE2.0" + bb += "\x03\x01" +# if loops == float('inf'): + if loops == float(0): + loops = 2**16-1 + bb += intToBin(loops) + bb += '\x00' # end + return bb + + +def getGraphicsControlExt(duration=0.1): + """ Graphics Control Extension. A sort of header at the start of + each image. Specifies transparancy and duration. """ + bb = '\x21\xF9\x04' + bb += '\x08' # no transparancy + bb += intToBin( int(duration*100) ) # in 100th of seconds + bb += '\x00' # no transparant color + bb += '\x00' # end + return bb + + +def _writeGifToFile(fp, images, durations, loops): + """ Given a set of images writes the bytes to the specified stream. + """ + # Obtain palette for all images and count each occurance + palettes, occur = [], [] + for im in images: + palettes.append( getheader(im)[1] ) + for palette in palettes: + occur.append( palettes.count( palette ) ) + + # Select most-used palette as the global one (or first in case no max) + globalPalette = palettes[ occur.index(max(occur)) ] + + # Init + frames = 0 + firstFrame = True + + + for im, palette in zip(images, palettes): + if firstFrame: + # Write header + + # Gather info + header = getheaderAnim(im) + appext = getAppExt(loops) + + # Write + fp.write(header) + fp.write(globalPalette) + fp.write(appext) + + # Next frame is not the first + firstFrame = False + + if True: + # Write palette and image data + + # Gather info + data = getdata(im) + imdes, data = data[0], data[1:] + graphext = getGraphicsControlExt(durations[frames]) + # Make image descriptor suitable for using 256 local color palette + lid = getImageDescriptor(im) + + # Write local header + if palette != globalPalette: + # Use local color palette + fp.write(graphext) + fp.write(lid) # write suitable image descriptor + fp.write(palette) # write local color table + fp.write('\x08') # LZW minimum size code + else: + # Use global color palette + fp.write(graphext) + fp.write(imdes) # write suitable image descriptor + + # Write image data + for d in data: + fp.write(d) + + # Prepare for next round + frames = frames + 1 + + fp.write(";") # end gif + return frames + + +## Exposed functions + +def writeGif(filename, images, duration=0.1, repeat=True, dither=False, nq=0): + """ writeGif(filename, images, duration=0.1, repeat=True, dither=False) + + Write an animated gif from the specified images. + + Parameters + ---------- + filename : string + The name of the file to write the image to. + images : list + Should be a list consisting of PIL images or numpy arrays. + The latter should be between 0 and 255 for integer types, and + between 0 and 1 for float types. + duration : scalar or list of scalars + The duration for all frames, or (if a list) for each frame. + repeat : bool or integer + The amount of loops. If True, loops infinitetely. + dither : bool + Whether to apply dithering + nq : integer + If nonzero, applies the NeuQuant quantization algorithm to create + the color palette. This algorithm is superior, but slower than + the standard PIL algorithm. The value of nq is the quality + parameter. 1 represents the best quality. 10 is in general a + good tradeoff between quality and speed. + + """ + progress_bar = progressbar.ProgressBar(maxval=len(images)) + progress_bar.start() + + # Check PIL + if PIL is None: + raise RuntimeError("Need PIL to write animated gif files.") + + # Check images + images = checkImages(images) + + # Check loops + if repeat is False: + loops = 1 + elif repeat is True: + loops = 0 # zero means infinite + else: + loops = int(repeat) + + # Convert to PIL images + images2 = [] + for im in images: + if isinstance(im, Image.Image): + images2.append(im) + elif np and isinstance(im, np.ndarray): + if im.ndim==3 and im.shape[2]==3: + im = Image.fromarray(im,'RGB') + elif im.ndim==2: + im = Image.fromarray(im,'L') + images2.append(im) + + # Convert to paletted PIL images + images, images2 = images2, [] + if nq >= 1: + # NeuQuant algorithm + for im in images: + im = im.convert("RGBA") # NQ assumes RGBA + NQ = NeuQuant(im, int(nq)) # Learn colors from image + if dither: + im = im.convert("RGB").quantize(palette=NQ.paletteImage()) + else: + im = NQ.quantize(im) # Use to quantize the image itself + images2.append(im) + else: + # Adaptive PIL algorithm + AD = Image.ADAPTIVE + count = 0 + for im in images: + progress_bar.update(count) + count += 1 + im = im.convert('P', palette=AD, dither=dither) + images2.append(im) + + # Check duration + if hasattr(duration, '__len__'): + if len(duration) == len(images2): + durations = [d for d in duration] + else: + raise ValueError("len(duration) doesn't match amount of images.") + else: + duration = [duration for im in images2] + + # Open file + fp = open(filename, 'wb') + + # Write + try: + n = _writeGifToFile(fp, images2, duration, loops) + finally: + fp.close() + progress_bar.finish() + + +def readGif(filename, asNumpy=True): + """ readGif(filename, asNumpy=True) + + Read images from an animated GIF file. Returns a list of numpy + arrays, or, if asNumpy is false, a list if PIL images. + + """ + + # Check PIL + if PIL is None: + raise RuntimeError("Need PIL to read animated gif files.") + + # Check Numpy + if np is None: + raise RuntimeError("Need Numpy to read animated gif files.") + + # Check whether it exists + if not os.path.isfile(filename): + raise IOError('File not found: '+str(filename)) + + # Load file using PIL + pilIm = PIL.Image.open(filename) + pilIm.seek(0) + + # Read all images inside + images = [] + try: + while True: + # Get image as numpy array + tmp = pilIm.convert() # Make without palette + a = np.asarray(tmp) + if len(a.shape)==0: + raise MemoryError("Too little memory to convert PIL image to array") + # Store, and next + images.append(a) + pilIm.seek(pilIm.tell()+1) + except EOFError: + pass + + # Convert to normal PIL images if needed + if not asNumpy: + images2 = images + images = [] + for im in images2: + images.append( PIL.Image.fromarray(im) ) + + # Done + return images + + +class NeuQuant: + """ NeuQuant(image, samplefac=10, colors=256) + + samplefac should be an integer number of 1 or higher, 1 + being the highest quality, but the slowest performance. + With avalue of 10, one tenth of all pixels are used during + training. This value seems a nice tradeof between speed + and quality. + + colors is the amount of colors to reduce the image to. This + should best be a power of two. + + See also: + http://members.ozemail.com.au/~dekker/NEUQUANT.HTML + + License of the NeuQuant Neural-Net Quantization Algorithm + --------------------------------------------------------- + + Copyright (c) 1994 Anthony Dekker + Ported to python by Marius van Voorden in 2010 + + NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. + See "Kohonen neural networks for optimal colour quantization" + in "network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. + for a discussion of the algorithm. + See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML + + Any party obtaining a copy of these files from the author, directly or + indirectly, is granted, free of charge, a full and unrestricted irrevocable, + world-wide, paid up, royalty-free, nonexclusive right and license to deal + in this software and documentation files (the "Software"), including without + limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons who receive + copies from any such party to do so, with the only requirement being + that this copyright notice remain intact. + + """ + + NCYCLES = None # Number of learning cycles + NETSIZE = None # Number of colours used + SPECIALS = None # Number of reserved colours used + BGCOLOR = None # Reserved background colour + CUTNETSIZE = None + MAXNETPOS = None + + INITRAD = None # For 256 colours, radius starts at 32 + RADIUSBIASSHIFT = None + RADIUSBIAS = None + INITBIASRADIUS = None + RADIUSDEC = None # Factor of 1/30 each cycle + + ALPHABIASSHIFT = None + INITALPHA = None # biased by 10 bits + + GAMMA = None + BETA = None + BETAGAMMA = None + + network = None # The network itself + colormap = None # The network itself + + netindex = None # For network lookup - really 256 + + bias = None # Bias and freq arrays for learning + freq = None + + pimage = None + + # Four primes near 500 - assume no image has a length so large + # that it is divisible by all four primes + PRIME1 = 499 + PRIME2 = 491 + PRIME3 = 487 + PRIME4 = 503 + MAXPRIME = PRIME4 + + pixels = None + samplefac = None + + a_s = None + + + def setconstants(self, samplefac, colors): + self.NCYCLES = 100 # Number of learning cycles + self.NETSIZE = colors # Number of colours used + self.SPECIALS = 3 # Number of reserved colours used + self.BGCOLOR = self.SPECIALS-1 # Reserved background colour + self.CUTNETSIZE = self.NETSIZE - self.SPECIALS + self.MAXNETPOS = self.NETSIZE - 1 + + self.INITRAD = self.NETSIZE/8 # For 256 colours, radius starts at 32 + self.RADIUSBIASSHIFT = 6 + self.RADIUSBIAS = 1 << self.RADIUSBIASSHIFT + self.INITBIASRADIUS = self.INITRAD * self.RADIUSBIAS + self.RADIUSDEC = 30 # Factor of 1/30 each cycle + + self.ALPHABIASSHIFT = 10 # Alpha starts at 1 + self.INITALPHA = 1 << self.ALPHABIASSHIFT # biased by 10 bits + + self.GAMMA = 1024.0 + self.BETA = 1.0/1024.0 + self.BETAGAMMA = self.BETA * self.GAMMA + + self.network = np.empty((self.NETSIZE, 3), dtype='float64') # The network itself + self.colormap = np.empty((self.NETSIZE, 4), dtype='int32') # The network itself + + self.netindex = np.empty(256, dtype='int32') # For network lookup - really 256 + + self.bias = np.empty(self.NETSIZE, dtype='float64') # Bias and freq arrays for learning + self.freq = np.empty(self.NETSIZE, dtype='float64') + + self.pixels = None + self.samplefac = samplefac + + self.a_s = {} + + def __init__(self, image, samplefac=10, colors=256): + + # Check Numpy + if np is None: + raise RuntimeError("Need Numpy for the NeuQuant algorithm.") + + # Check image + if image.size[0] * image.size[1] < NeuQuant.MAXPRIME: + raise IOError("Image is too small") + assert image.mode == "RGBA" + + # Initialize + self.setconstants(samplefac, colors) + self.pixels = np.fromstring(image.tostring(), np.uint32) + self.setUpArrays() + + self.learn() + self.fix() + self.inxbuild() + + def writeColourMap(self, rgb, outstream): + for i in range(self.NETSIZE): + bb = self.colormap[i,0]; + gg = self.colormap[i,1]; + rr = self.colormap[i,2]; + out.write(rr if rgb else bb) + out.write(gg) + out.write(bb if rgb else rr) + return self.NETSIZE + + def setUpArrays(self): + self.network[0,0] = 0.0 # Black + self.network[0,1] = 0.0 + self.network[0,2] = 0.0 + + self.network[1,0] = 255.0 # White + self.network[1,1] = 255.0 + self.network[1,2] = 255.0 + + # RESERVED self.BGCOLOR # Background + + for i in range(self.SPECIALS): + self.freq[i] = 1.0 / self.NETSIZE + self.bias[i] = 0.0 + + for i in range(self.SPECIALS, self.NETSIZE): + p = self.network[i] + p[:] = (255.0 * (i-self.SPECIALS)) / self.CUTNETSIZE + + self.freq[i] = 1.0 / self.NETSIZE + self.bias[i] = 0.0 + + # Omitted: setPixels + + def altersingle(self, alpha, i, b, g, r): + """Move neuron i towards biased (b,g,r) by factor alpha""" + n = self.network[i] # Alter hit neuron + n[0] -= (alpha*(n[0] - b)) + n[1] -= (alpha*(n[1] - g)) + n[2] -= (alpha*(n[2] - r)) + + def geta(self, alpha, rad): + try: + return self.a_s[(alpha, rad)] + except KeyError: + length = rad*2-1 + mid = length/2 + q = np.array(range(mid-1,-1,-1)+range(-1,mid)) + a = alpha*(rad*rad - q*q)/(rad*rad) + a[mid] = 0 + self.a_s[(alpha, rad)] = a + return a + + def alterneigh(self, alpha, rad, i, b, g, r): + if i-rad >= self.SPECIALS-1: + lo = i-rad + start = 0 + else: + lo = self.SPECIALS-1 + start = (self.SPECIALS-1 - (i-rad)) + + if i+rad <= self.NETSIZE: + hi = i+rad + end = rad*2-1 + else: + hi = self.NETSIZE + end = (self.NETSIZE - (i+rad)) + + a = self.geta(alpha, rad)[start:end] + + p = self.network[lo+1:hi] + p -= np.transpose(np.transpose(p - np.array([b, g, r])) * a) + + #def contest(self, b, g, r): + # """ Search for biased BGR values + # Finds closest neuron (min dist) and updates self.freq + # finds best neuron (min dist-self.bias) and returns position + # for frequently chosen neurons, self.freq[i] is high and self.bias[i] is negative + # self.bias[i] = self.GAMMA*((1/self.NETSIZE)-self.freq[i])""" + # + # i, j = self.SPECIALS, self.NETSIZE + # dists = abs(self.network[i:j] - np.array([b,g,r])).sum(1) + # bestpos = i + np.argmin(dists) + # biasdists = dists - self.bias[i:j] + # bestbiaspos = i + np.argmin(biasdists) + # self.freq[i:j] -= self.BETA * self.freq[i:j] + # self.bias[i:j] += self.BETAGAMMA * self.freq[i:j] + # self.freq[bestpos] += self.BETA + # self.bias[bestpos] -= self.BETAGAMMA + # return bestbiaspos + def contest(self, b, g, r): + """ Search for biased BGR values + Finds closest neuron (min dist) and updates self.freq + finds best neuron (min dist-self.bias) and returns position + for frequently chosen neurons, self.freq[i] is high and self.bias[i] is negative + self.bias[i] = self.GAMMA*((1/self.NETSIZE)-self.freq[i])""" + i, j = self.SPECIALS, self.NETSIZE + dists = abs(self.network[i:j] - np.array([b,g,r])).sum(1) + bestpos = i + np.argmin(dists) + biasdists = dists - self.bias[i:j] + bestbiaspos = i + np.argmin(biasdists) + self.freq[i:j] *= (1-self.BETA) + self.bias[i:j] += self.BETAGAMMA * self.freq[i:j] + self.freq[bestpos] += self.BETA + self.bias[bestpos] -= self.BETAGAMMA + return bestbiaspos + + + + + def specialFind(self, b, g, r): + for i in range(self.SPECIALS): + n = self.network[i] + if n[0] == b and n[1] == g and n[2] == r: + return i + return -1 + + def learn(self): + biasRadius = self.INITBIASRADIUS + alphadec = 30 + ((self.samplefac-1)/3) + lengthcount = self.pixels.size + samplepixels = lengthcount / self.samplefac + delta = samplepixels / self.NCYCLES + alpha = self.INITALPHA + + i = 0; + rad = biasRadius >> self.RADIUSBIASSHIFT + if rad <= 1: + rad = 0 + + print "Beginning 1D learning: samplepixels =",samplepixels," rad =", rad + + step = 0 + pos = 0 + if lengthcount%NeuQuant.PRIME1 != 0: + step = NeuQuant.PRIME1 + elif lengthcount%NeuQuant.PRIME2 != 0: + step = NeuQuant.PRIME2 + elif lengthcount%NeuQuant.PRIME3 != 0: + step = NeuQuant.PRIME3 + else: + step = NeuQuant.PRIME4 + + i = 0 + printed_string = '' + while i < samplepixels: + if i%100 == 99: + tmp = '\b'*len(printed_string) + printed_string = str((i+1)*100/samplepixels)+"%\n" + print tmp + printed_string, + p = self.pixels[pos] + r = (p >> 16) & 0xff + g = (p >> 8) & 0xff + b = (p ) & 0xff + + if i == 0: # Remember background colour + self.network[self.BGCOLOR] = [b, g, r] + + j = self.specialFind(b, g, r) + if j < 0: + j = self.contest(b, g, r) + + if j >= self.SPECIALS: # Don't learn for specials + a = (1.0 * alpha) / self.INITALPHA + self.altersingle(a, j, b, g, r) + if rad > 0: + self.alterneigh(a, rad, j, b, g, r) + + pos = (pos+step)%lengthcount + + i += 1 + if i%delta == 0: + alpha -= alpha / alphadec + biasRadius -= biasRadius / self.RADIUSDEC + rad = biasRadius >> self.RADIUSBIASSHIFT + if rad <= 1: + rad = 0 + print "Finished 1D learning: final alpha =",(1.0*alpha)/self.INITALPHA,"!" + + def fix(self): + for i in range(self.NETSIZE): + for j in range(3): + x = int(0.5 + self.network[i,j]) + x = max(0, x) + x = min(255, x) + self.colormap[i,j] = x + self.colormap[i,3] = i + + def inxbuild(self): + previouscol = 0 + startpos = 0 + for i in range(self.NETSIZE): + p = self.colormap[i] + q = None + smallpos = i + smallval = p[1] # Index on g + # Find smallest in i..self.NETSIZE-1 + for j in range(i+1, self.NETSIZE): + q = self.colormap[j] + if q[1] < smallval: # Index on g + smallpos = j + smallval = q[1] # Index on g + + q = self.colormap[smallpos] + # Swap p (i) and q (smallpos) entries + if i != smallpos: + p[:],q[:] = q, p.copy() + + # smallval entry is now in position i + if smallval != previouscol: + self.netindex[previouscol] = (startpos+i) >> 1 + for j in range(previouscol+1, smallval): + self.netindex[j] = i + previouscol = smallval + startpos = i + self.netindex[previouscol] = (startpos+self.MAXNETPOS) >> 1 + for j in range(previouscol+1, 256): # Really 256 + self.netindex[j] = self.MAXNETPOS + + + def paletteImage(self): + """ PIL weird interface for making a paletted image: create an image which + already has the palette, and use that in Image.quantize. This function + returns this palette image. """ + if self.pimage is None: + palette = [] + for i in range(self.NETSIZE): + palette.extend(self.colormap[i][:3]) + + palette.extend([0]*(256-self.NETSIZE)*3) + + # a palette image to use for quant + self.pimage = Image.new("P", (1, 1), 0) + self.pimage.putpalette(palette) + return self.pimage + + + def quantize(self, image): + """ Use a kdtree to quickly find the closest palette colors for the pixels """ + if cKDTree: + return self.quantize_with_scipy(image) + else: + print 'Scipy not available, falling back to slower version.' + return self.quantize_without_scipy(image) + + + def quantize_with_scipy(self, image): + w,h = image.size + px = np.asarray(image).copy() + px2 = px[:,:,:3].reshape((w*h,3)) + + kdtree = cKDTree(self.colormap[:,:3],leafsize=10) + result = kdtree.query(px2) + colorindex = result[1] + print "Distance:", (result[0].sum()/(w*h)) + px2[:] = self.colormap[colorindex,:3] + + return Image.fromarray(px).convert("RGB").quantize(palette=self.paletteImage()) + + + def quantize_without_scipy(self, image): + """" This function can be used if no scipy is availabe. + It's 7 times slower though. + """ + w,h = image.size + px = np.asarray(image).copy() + memo = {} + for j in range(w): + for i in range(h): + key = (px[i,j,0],px[i,j,1],px[i,j,2]) + try: + val = memo[key] + except KeyError: + val = self.convert(key) + memo[key] = val + px[i,j,0],px[i,j,1],px[i,j,2] = val + return Image.fromarray(px).convert("RGB").quantize(palette=self.paletteImage()) + + def convert(self, (r, g, b)): + i = self.inxsearch(r, g, b) + return self.colormap[i,:3] + + def inxsearch(self, r, g, b): + """Search for BGR values 0..255 and return colour index""" + dists = (self.colormap[:,:3] - np.array([r,g,b])) + a= np.argmin((dists*dists).sum(1)) + return a + + + +if __name__ == '__main__': + im = np.zeros((200,200), dtype=np.uint8) + im[10:30,:] = 100 + im[:,80:120] = 255 + im[-50:-40,:] = 50 + + images = [im*1.0, im*0.8, im*0.6, im*0.4, im*0] + writeGif('lala3.gif',images, duration=0.5, dither=0) \ No newline at end of file diff --git a/mobject.py b/mobject.py new file mode 100644 index 00000000..1acc99a2 --- /dev/null +++ b/mobject.py @@ -0,0 +1,555 @@ +import numpy as np +import itertools as it +import os +from PIL import Image +from random import random + +from animate import * +from tex_image_utils import NAME_TO_IMAGE_FILE +import displayer as disp + +class Mobject(object): + """ + Mathematical Object + """ + #Number of numbers used to describe a point (3 for pos, 3 for normal vector) + DIM = 3 + + DEFAULT_COLOR = Color("skyblue") + + SHOULD_BUFF_POINTS = GENERALLY_BUFF_POINTS + + def __init__(self, + color = None, + name = None, + center = None, + ): + self.color = Color(color) if color else Color(self.DEFAULT_COLOR) + if not hasattr(self, "name"): + self.name = name or self.__class__.__name__ + self.has_normals = hasattr(self, 'unit_normal') + self.points = np.zeros((0, 3)) + self.rgbs = np.zeros((0, 3)) + if self.has_normals: + self.unit_normals = np.zeros((0, 3)) + self.generate_points() + if center: + self.center().shift(center) + + def __str__(self): + return self.name + + def display(self): + disp.get_image(self.points, self.rgbs).show() + + def save_image(self, name = None): + disp.get_image(self.points, self.rgbs).save( + os.path.join(MOVIE_DIR, (name or str(self)) + ".png") + ) + + def add_points(self, points, rgbs = None, color = None): + """ + points must be a Nx3 numpy array, as must rgbs if it is not None + """ + points = np.array(points) + num_new_points = points.shape[0] + self.points = np.append(self.points, points) + self.points = self.points.reshape((self.points.size / 3, 3)) + if rgbs is None: + color = Color(color) if color else self.color + rgbs = np.array([color.get_rgb()] * num_new_points) + else: + if rgbs.shape != points.shape: + raise Exception("points and rgbs must have same shape") + self.rgbs = np.append(self.rgbs, rgbs).reshape(self.points.shape) + if self.has_normals: + self.unit_normals = np.append( + self.unit_normals, + np.array([self.unit_normal(point) for point in points]) + ).reshape(self.points.shape) + return self + + def rotate(self, angle, axis = [0, 0, 1]): + t_rotation_matrix = np.transpose(rotation_matrix(angle, axis)) + self.points = np.dot(self.points, t_rotation_matrix) + if self.has_normals: + self.unit_normals = np.dot(self.unit_normals, t_rotation_matrix) + return self + + def shift(self, vector): + cycle = it.cycle(vector) + v = np.array([cycle.next() for x in range(self.points.size)]).reshape(self.points.shape) + self.points += v + return self + + def center(self): + self.shift(-self.get_center()) + return self + + def get_center(self): + return np.apply_along_axis(np.mean, 0, self.points) + + def scale(self, scale_factor): + self.points *= scale_factor + return self + + def add(self, *mobjects): + for mobject in mobjects: + self.add_points(mobject.points, mobject.rgbs) + return self + + def get_num_points(self): + return self.points.shape[0] + + def pose_at_angle(self): + self.rotate(np.pi / 7) + self.rotate(np.pi / 7, [1, 0, 0]) + return self + + def apply_function(self, function): + self.points = np.apply_along_axis(function, 1, self.points) + return self + + def apply_complex_function(self, function): + def point_map((x, y, z)): + result = function(complex(x, y)) + return (result.real, result.imag, 0) + return self.apply_function(point_map) + + def highlight(self, color = "red", condition = lambda x : True): + """ + Condition is function which takes in one arguments, (x, y, z). + """ + to_change = np.apply_along_axis(condition, 1, self.points) + self.rgbs[to_change, :] *= 0 + self.rgbs[to_change, :] += Color(color).get_rgb() + return self + + def fade(self, amount = 0.5): + self.rgbs += amount + return self + + def filter_out(self, condition): + to_eliminate = ~np.apply_along_axis(condition, 1, self.points) + self.points = self.points[to_eliminate] + self.rgbs = self.rgbs[to_eliminate] + return self + + def generate_points(self): + #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() + if count1 == 0: + mobject1.add_points([(0, 0, 0)]) + if count2 == 0: + mobject2.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]): + setattr(mobject, attr, np.array(array)) + + def interpolate(mobject1, mobject2, target_mobject, alpha): + """ + Turns target_mobject into an interpolation between mobject1 + and mobject2. + """ + 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) + +class Mobject1D(Mobject): + def __init__(self, density = DEFAULT_POINT_DENSITY_1D, *args, **kwargs): + self.epsilon = 1.0 / density + Mobject.__init__(self, *args, **kwargs) + +class Mobject2D(Mobject): + def __init__(self, density = DEFAULT_POINT_DENSITY_2D, *args, **kwargs): + self.epsilon = 1.0 / density + Mobject.__init__(self, *args, **kwargs) + +class CompoundMobject(Mobject): + def __init__(self, *mobjects): + Mobject.__init__(self) + for mobject in mobjects: + self.add_points(mobject.points, mobject.rgbs) + + +###### Concrete Mobjects ######## + +class Stars(Mobject): + DEFAULT_COLOR = "white" + SHOULD_BUFF_POINTS = False + def __init__(self, num_points = DEFAULT_NUM_STARS, + *args, **kwargs): + self.num_points = num_points + Mobject.__init__(self, *args, **kwargs) + + def generate_points(self): + self.add_points([ + ( + r * np.sin(phi)*np.cos(theta), + r * np.sin(phi)*np.sin(theta), + r * np.cos(phi) + ) + for x in range(self.num_points) + for r, phi, theta in [[ + max(SPACE_HEIGHT, SPACE_WIDTH) * random(), + np.pi * random(), + 2 * np.pi * random(), + ]] + ]) + +class Point(Mobject): + def __init__(self, point = (0, 0, 0), *args, **kwargs): + Mobject.__init__(self, *args, **kwargs) + self.points = np.array(point).reshape(1, 3) + self.rgbs = np.array(self.color.get_rgb()).reshape(1, 3) + +class Arrow(Mobject1D): + NUNGE_DISTANCE = 0.1 + def __init__(self, point = (0, 0, 0), direction = (-1, 1, 0), + length = 1, tip_length = 0.25, + normal = (0, 0, 1), *args, **kwargs): + self.point = np.array(point) + self.direction = np.array(direction) / np.linalg.norm(direction) + self.normal = np.array(normal) + self.length = length + self.tip_length = tip_length + Mobject1D.__init__(self, *args, **kwargs) + + def generate_points(self): + self.add_points([ + [x, x, x] * self.direction + self.point + for x in np.arange(-self.length, 0, self.epsilon) + ]) + tips_dir = np.array(-self.direction), np.array(-self.direction) + for i, sgn in zip([0, 1], [-1, 1]): + rotate_vector(tips_dir[i], sgn * np.pi / 4, self.normal) + self.add_points([ + [x, x, x] * tips_dir[i] + self.point + for x in np.arange(0, self.tip_length, self.epsilon) + for i in [0, 1] + ]) + + def nudge(self): + return self.shift(-self.direction * self.NUNGE_DISTANCE) + +class Vector(Arrow): + def __init__(self, point = (1, 0, 0), *args, **kwargs): + length = np.linalg.norm(point) + Arrow.__init__(self, point = point, direction = point, + length = length, tip_length = 0.2 * length, + *args, **kwargs) + +class Dot(Mobject1D): + DEFAULT_COLOR = "white" + def __init__(self, center = (0, 0, 0), radius = 0.05, *args, **kwargs): + self.center = center + self.radius = radius + Mobject1D.__init__(self, *args, **kwargs) + + def generate_points(self): + self.add_points([ + np.array((t*np.cos(theta), t*np.sin(theta), 0)) + self.center + for t in np.arange(0, self.radius, self.epsilon) + for theta in np.arange(0, 2 * np.pi, self.epsilon) + ]) + +class Cross(Mobject1D): + RADIUS = 0.3 + DEFAULT_COLOR = "white" + 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] + ]) + +class Line(Mobject1D): + def __init__(self, start, end, density = DEFAULT_POINT_DENSITY_1D, *args, **kwargs): + self.start = np.array(start) + self.end = np.array(end) + density *= np.linalg.norm(self.start - self.end) + Mobject1D.__init__(self, density = density, *args, **kwargs) + + def generate_points(self): + self.add_points([ + t * self.end + (1 - t) * self.start + for t in np.arange(0, 1, self.epsilon) + ]) + + +class Cube(Mobject2D): + def generate_points(self): + self.add_points([ + sgn * np.array(coords) + for x in np.arange(-1, 1, self.epsilon) + for y in np.arange(x, 1, self.epsilon) + for coords in it.permutations([x, y, 1]) + for sgn in [-1, 1] + ]) + self.pose_at_angle() + + def unit_normal(self, coords): + return np.array(map(lambda x : 1 if abs(x) == 1 else 0, coords)) + +class CubeShell(Mobject1D): + DEFAULT_COLOR = "yellow" + def generate_points(self): + self.add_points([ + ([a, b, c][p[0]], [a, b, c][p[1]], [a, b, c][p[2]]) + for p in [(0, 1, 2), (2, 0, 1), (1, 2, 0)] + for a, b, c in it.product([-1, 1], [-1, 1], np.arange(-1, 1, self.epsilon)) + ]) + self.pose_at_angle() + + +class Sphere(Mobject2D): + def generate_points(self): + self.add_points([ + ( + np.sin(phi) * np.cos(theta), + np.sin(phi) * np.sin(theta), + np.cos(phi) + ) + for phi in np.arange(self.epsilon, np.pi, self.epsilon) + for theta in np.arange(0, 2 * np.pi, 2 * self.epsilon / np.sin(phi)) + ]) + + def unit_normal(self, coords): + return np.array(coords) / np.linalg.norm(coords) + +class Circle(Mobject1D): + DEFAULT_COLOR = "red" + def generate_points(self): + self.add_points([ + (np.cos(theta), np.sin(theta), 0) + for theta in np.arange(0, 2 * np.pi, self.epsilon) + ]) + + def repeat(self, count): + #Can make transition animations quite pretty + for x in range(count - 1): + self.add_points(self.points) + return self + +class FunctionGraph(Mobject1D): + DEFAULT_COLOR = "lightblue" + def __init__(self, function, x_range = [-10, 10], *args, **kwargs): + self.function = function + self.x_min = x_range[0] / SPACE_WIDTH + self.x_max = x_range[1] / SPACE_WIDTH + Mobject1D.__init__(self, *args, **kwargs) + + def generate_points(self): + scale_factor = 2.0 * SPACE_WIDTH / (self.x_max - self.x_min) + self.epsilon /= scale_factor + self.add_points([ + np.array([x, self.function(x), 0]) + for x in np.arange(self.x_min, self.x_max, self.epsilon) + ]) + self.scale(scale_factor) + + +class ParametricFunction(Mobject): + DEFAULT_COLOR = "lightblue" + def __init__(self, + function, + dim = 1, + expected_measure = 10.0, + density = None, + *args, + **kwargs): + self.function = function + self.dim = dim + self.expected_measure = expected_measure + if density: + self.epsilon = 1.0 / density + elif self.dim == 1: + self.epsilon = 1.0 / expected_measure / DEFAULT_POINT_DENSITY_1D + else: + self.epsilon = 1.0 / np.sqrt(expected_measure) / DEFAULT_POINT_DENSITY_2D + Mobject.__init__(self, *args, **kwargs) + + def generate_points(self): + if self.dim == 1: + self.add_points([ + self.function(t) + for t in np.arange(-1, 1, self.epsilon) + ]) + if self.dim == 2: + self.add_points([ + self.function(s, t) + for t in np.arange(-1, 1, self.epsilon) + for s in np.arange(-1, 1, self.epsilon) + ]) + +class Grid(Mobject1D): + DEFAULT_COLOR = "green" + def __init__(self, + radius = max(SPACE_HEIGHT, SPACE_WIDTH), + interval_size = 1.0, + subinterval_size = 0.5, + *args, **kwargs): + self.radius = radius + self.interval_size = interval_size + self.subinterval_size = subinterval_size + Mobject1D.__init__(self, *args, **kwargs) + + def generate_points(self): + self.add_points([ + (sgns[0] * x, sgns[1] * y, 0) + for beta in np.arange(0, self.radius, self.interval_size) + for alpha in np.arange(0, self.radius, self.epsilon) + for sgns in it.product((-1, 1), (-1, 1)) + for x, y in [(alpha, beta), (beta, alpha)] + ]) + if self.subinterval_size: + si = self.subinterval_size + color = Color(self.color) + color.set_rgb([x/2 for x in color.get_rgb()]) + self.add_points([ + (sgns[0] * x, sgns[1] * y, 0) + for beta in np.arange(0, self.radius, si) + if abs(beta % self.interval_size) > self.epsilon + for alpha in np.arange(0, self.radius, self.epsilon) + for sgns in it.product((-1, 1), (-1, 1)) + for x, y in [(alpha, beta), (beta, alpha)] + ], color = color) + +class NumberLine(Mobject1D): + def __init__(self, + radius = SPACE_WIDTH, + interval_size = 0.5, tick_size = 0.1, + with_numbers = False, *args, **kwargs): + self.radius = int(radius) + 1 + self.interval_size = interval_size + self.tick_size = tick_size + self.with_numbers = with_numbers + Mobject1D.__init__(self, *args, **kwargs) + + def generate_points(self): + self.add_points([ + (x, 0, 0) + for x in np.arange(-self.radius, self.radius, self.epsilon) + ]) + self.add_points([ + (0, y, 0) + for y in np.arange(-2*self.tick_size, 2*self.tick_size, self.epsilon) + ]) + self.add_points([ + (x, y, 0) + for x in np.arange(-self.radius, self.radius, self.interval_size) + for y in np.arange(-self.tick_size, self.tick_size, self.epsilon) + ]) + if self.with_numbers: #TODO, make these numbers a separate object + vertical_displacement = -0.3 + max_explicit_num = 3 + num_to_name = dict( + (x, str(x)) + for x in range(-max_explicit_num, max_explicit_num + 1) + ) + num_to_name[max_explicit_num + 1] = "cdots" + num_to_name[-max_explicit_num - 1] = "cdots" + nums = CompoundMobject(*[ + ImageMobject( + NAME_TO_IMAGE_FILE[num_to_name[x]] + ).scale(0.6).center().shift( + [x * self.interval_size, vertical_displacement, 0] + ) + for x in range(-max_explicit_num - 1, max_explicit_num + 2) + ]) + self.add_points(nums.points, nums.rgbs) + +# class ComplexPlane(Grid): +# def __init__(self, *args, **kwargs): +# Grid.__init__(self, *args, **kwargs) +# self.add(Dot()) + + + + +class ImageMobject(Mobject2D): + """ + Automatically filters out black pixels + """ +# SHOULD_BUFF_POINTS = False + def __init__(self, + image, + filter_color = "black", + invert = True, + *args, **kwargs): + #TODO, Make sure you always convert to RGB + self.filter_rgb = 255 * np.array(Color(filter_color).get_rgb()).astype('uint8') + if isinstance(image, str): + self.name = to_cammel_case( + os.path.split(image)[-1].split(".")[0] + ) + possible_paths = [ + image, + os.path.join(IMAGE_DIR, image), + os.path.join(IMAGE_DIR, image + ".jpg"), + os.path.join(IMAGE_DIR, image + ".png"), + ] + found = False + for path in possible_paths: + if os.path.exists(path): + image = Image.open(path).convert('RGB') + found = True + if not found: + raise IOError("File not Found") + if invert: + image = invert_image(image) + self.image_array = np.array(image) + Mobject2D.__init__(self, *args, **kwargs) + + def generate_points(self): + height, width = self.image_array.shape[:2] + #Flatten array, and find indices where rgb is not filter_rgb + array = self.image_array.reshape((height * width, 3)) + ones = np.ones(height * width, dtype = 'bool') + for i in range(3): + ones *= (array[:,i] != self.filter_rgb[i]) + indices = np.arange(height * width, dtype = 'int')[ones] + rgbs = array[indices, :].astype('float') / 255.0 + + points = np.array([ + ( + i%width - (width / 2.0), + -i/width + (height / 2.0), #flip y-axis + 0 + ) + for i in indices + ], dtype = 'float64') + height, width = map(float, (height, width)) + if height / width > float(HEIGHT) / WIDTH: + points *= 2 * SPACE_HEIGHT / height + else: + points *= 2 * SPACE_WIDTH / width + self.add_points(points, rgbs = rgbs) + + + + + + + + + + + + + + + + + + + diff --git a/tex_image_utils.py b/tex_image_utils.py new file mode 100644 index 00000000..5051a6de --- /dev/null +++ b/tex_image_utils.py @@ -0,0 +1,70 @@ +import os +from PIL import Image +from constants import PDF_DIR, IMAGE_DIR, WIDTH, HEIGHT, PDF_DENSITY + +def load_pdf_images(filename, regen_if_exists = False): + """ + Converts a pdf, 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 pdf + """ + #TODO, Handle case where there is one page in the pdf! + possible_paths = [ + filename, + os.path.join(PDF_DIR, filename), + os.path.join(PDF_DIR, filename + ".pdf"), + ] + for path in possible_paths: + if os.path.exists(path): + directory, filename = os.path.split(path) + name = filename.split(".")[0] + images_dir = os.path.join(IMAGE_DIR, name) + already_exists = os.path.exists(images_dir) + if not already_exists: + os.mkdir(images_dir) + if not already_exists or regen_if_exists: + commands = [ + "convert", + "-density", + str(PDF_DENSITY), + path, + "-size", + str(WIDTH) + "x" + str(HEIGHT), + os.path.join(images_dir, name + ".png") + ] + os.system(" ".join(commands)) + image_paths = [ + os.path.join(images_dir, name) + for name in os.listdir(images_dir) + if name.endswith(".png") + ] + image_paths.sort(cmp_enumerated_files) + return [Image.open(path).convert('RGB') for path in image_paths] + raise IOError("File not Found") + +def cmp_enumerated_files(name1, name2): + num1, num2 = [ + int(name.split(".")[0].split("-")[-1]) + for name in (name1, name2) + ] + return num1 - num2 + +SYMBOL_IMAGES = load_pdf_images("symbols.pdf", regen_if_exists = False) + +NAME_TO_IMAGE_FILE = dict( + zip([ + "-3", + "-2", + "-1", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "cdots", + "3Blue1Brown", + ], SYMBOL_IMAGES) +) \ No newline at end of file