diff --git a/constants.py b/constants.py index 9729190c..9f3b09f3 100644 --- a/constants.py +++ b/constants.py @@ -34,7 +34,8 @@ for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TEX_DIR, TMP_IMAGE_DIR, TEX_IMAGE_ PDF_DENSITY = 800 SIZE_TO_REPLACE = "SizeHere" TEX_TEXT_TO_REPLACE = "YourTextHere" -TEMPLATE_TEX_FILE = os.path.join(TEX_DIR, "template.tex") +TEMPLATE_TEX_FILE = os.path.join(TEX_DIR, "template.tex") +TEMPLATE_TEXT_FILE = os.path.join(TEX_DIR, "text_template.tex") LOGO_PATH = os.path.join(IMAGE_DIR, "logo.png") diff --git a/helpers.py b/helpers.py index 397a083e..b5efa13e 100644 --- a/helpers.py +++ b/helpers.py @@ -72,6 +72,9 @@ def there_and_back(t, inflection = 10.0): def not_quite_there(t, proportion = 0.7): return proportion*high_inflection_0_to_1(t) +def wiggle(t, wiggles = 2): + return there_and_back(t) * np.sin(wiggles*np.pi*t) + ### Functional Functions ### def composition(func_list): diff --git a/image_mobject.py b/image_mobject.py index c0e419bc..5e8a95a8 100644 --- a/image_mobject.py +++ b/image_mobject.py @@ -86,11 +86,16 @@ class VideoIcon(ImageMobject): ImageMobject.__init__(self, "video_icon", *args, **kwargs) self.scale(0.3) +def text_mobject(text, size = r"\Large"): + image = tex_to_image(text, size, TEMPLATE_TEXT_FILE) + assert(not isinstance(image, list)) + return ImageMobject(image) + #Purely redundant function to make singulars and plurals sensible -def tex_mobject(expression, size = "\HUGE"): +def tex_mobject(expression, size = r"\HUGE"): return tex_mobjects(expression, size) -def tex_mobjects(expression, size = "\HUGE"): +def tex_mobjects(expression, size = r"\HUGE"): images = tex_to_image(expression, size) if isinstance(images, list): #TODO, is checking listiness really the best here? diff --git a/mobject.py b/mobject.py new file mode 100644 index 00000000..ae03477a --- /dev/null +++ b/mobject.py @@ -0,0 +1,511 @@ +import numpy as np +import itertools as it +import os +from PIL import Image +from random import random +from copy import deepcopy +from colour import Color + +from constants import * +from helpers import * +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 show(self): + Image.fromarray(disp.paint_mobject(self)).show() + + def save_image(self, name = None): + Image.fromarray(disp.paint_mobject(self)).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 rotate_in_place(self, angle, axis = [0, 0, 1]): + center = self.get_center() + self.shift(-center) + self.rotate(angle, axis) + self.shift(center) + 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 scale(self, scale_factor): + self.points *= scale_factor + return self + + def scale_in_place(self, scale_factor): + center = self.get_center() + return self.center().scale(scale_factor).shift(center) + + def add(self, *mobjects): + for mobject in mobjects: + self.add_points(mobject.points, mobject.rgbs) + return self + + def repeat(self, count): + #Can make transition animations nicer + points, rgbs = deepcopy(self.points), deepcopy(self.rgbs) + for x in range(count - 1): + self.add_points(points, 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). + """ + #TODO, Should self.color change? + 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 + + ### Getters ### + def get_center(self): + return np.apply_along_axis(np.mean, 0, self.points) + + def get_width(self): + return np.max(self.points[:, 0]) - np.min(self.points[:, 0]) + + def get_height(self): + return np.max(self.points[:, 1]) - np.min(self.points[:, 1]) + + ### Stuff subclasses should deal with ### + def should_buffer_points(self): + # potentially changed in subclasses + return GENERALLY_BUFF_POINTS + + 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): + DEFAULT_COLOR = "white" + 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]): + tips_dir[i] = 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): #Use 1D density, even though 2D + DEFAULT_COLOR = "white" + def __init__(self, center = (0, 0, 0), radius = 0.05, *args, **kwargs): + center = np.array(center) + if center.size == 1: + raise Exception("Center must have 2 or 3 coordinates!") + elif center.size == 2: + center = np.append(center, [0]) + self.center_point = 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_point + 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 CurvedLine(Line): + def generate_points(self): + equidistant_point = rotate_vector( + self.end - self.start, + np.pi/3, [0,0,1] + ) + self.start + self.add_points([ + (1 - t*(1-t))*(t*self.end + (1-t)*self.start) \ + + t*(1-t)*equidistant_point + for t in np.arange(0, 1, self.epsilon) + ]) + self.ep = equidistant_point + +class CubeWithFaces(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 Cube(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) + ]) + +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, test + vertical_displacement = -0.3 + nums = range(-self.radius, self.radius) + nums = map(lambda x : x / self.interval_size, nums) + mobs = tex_mobjects(*[str(num) for num in nums]) + for num, mob in zip(nums, mobs): + mob.center().shift([num, vertical_displacement, 0]) + self.add(*mobs) + +# class ComplexPlane(Grid): +# def __init__(self, *args, **kwargs): +# Grid.__init__(self, *args, **kwargs) +# self.add(Dot()) \ No newline at end of file diff --git a/mobject_movement.py b/mobject_movement.py deleted file mode 100644 index 3ed9e5b3..00000000 --- a/mobject_movement.py +++ /dev/null @@ -1,345 +0,0 @@ -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 MobjectMovement(object): - def __init__(self, - mobject, - run_time = DEFAULT_ANIMATION_RUN_TIME, - alpha_func = high_inflection_0_to_1, - name = None): - if isinstance(mobject, type) and issubclass(mobject, Mobject): - self.mobject = mobject() - elif isinstance(mobject, Mobject): - self.mobject = mobject - else: - raise Exception("Invalid mobject parameter, must be \ - subclass or instance of Mobject") - self.starting_mobject = copy.deepcopy(self.mobject) - self.reference_mobjects = [self.starting_mobject] - self.alpha_func = alpha_func or (lambda x : x) - self.run_time = run_time - #TODO, Adress the idea of filtering the mobmov - self.filter_functions = [] - self.restricted_height = SPACE_HEIGHT - self.restricted_width = SPACE_WIDTH - self.spacial_center = np.zeros(3) - self.name = name or self.__class__.__name__ + str(self.mobject) - - def __str__(self): - return 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 - """ - #TODO, I don't think this should be necessary. This should happen - #under the individual mobjects. - points = self.mobject.points - rgbs = self.mobject.rgbs - #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, alpha): - if alpha < 0: - alpha = 0 - if alpha > 1: - alpha = 1 - self.update_mobject(self.alpha_func(alpha)) - - 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 - return self - - def set_run_time(self, time): - self.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.name = name - 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 update_mobject(self, alpha): - #Typically ipmlemented by subclass - pass - - def clean_up(self): - pass - - -###### Concrete MobjectMovement ######## - -class Rotating(MobjectMovement): - def __init__(self, - mobject, - axis = None, - axes = [[0, 0, 1], [0, 1, 0]], - radians = 2 * np.pi, - run_time = 20.0, - alpha_func = None, - *args, **kwargs): - MobjectMovement.__init__( - self, mobject, - run_time = run_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, axis = (0, 0, 1), axes = None, - run_time = DEFAULT_ANIMATION_RUN_TIME, - alpha_func = high_inflection_0_to_1, - *args, **kwargs): - Rotating.__init__( - self, - mobject, - axis = axis, - axes = axes, - run_time = run_time, - radians = radians, - alpha_func = alpha_func, - ) - -class FadeOut(MobjectMovement): - def update_mobject(self, alpha): - self.mobject.rgbs = self.starting_mobject.rgbs * (1 - alpha) - -class Reveal(MobjectMovement): - 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(MobjectMovement): - 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) - MobjectMovement.__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(MobjectMovement): - def __init__(self, homotopy, *args, **kwargs): - """ - Homotopy a function from (x, y, z, t) to (x', y', z') - """ - self.homotopy = homotopy - MobjectMovement.__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(MobjectMovement): - def update_mobject(self, alpha): - #TODO, shoudl I make this more efficient? - 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(MobjectMovement): - def __init__(self, mobject, color = "white", slow_factor = 0.01, - run_time = 0.1, alpha_func = None, - *args, **kwargs): - MobjectMovement.__init__(self, mobject, run_time = run_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/moser/main.py b/moser/main.py index 64595401..152c32f6 100644 --- a/moser/main.py +++ b/moser/main.py @@ -17,57 +17,9 @@ from scene import Scene from moser_helpers import * from graphs import * -RADIUS = SPACE_HEIGHT - 0.1 -CIRCLE_DENSITY = DEFAULT_POINT_DENSITY_1D*RADIUS movie_prefix = "moser/" -############################################ - -class CircleScene(Scene): - def __init__(self, radians, *args, **kwargs): - Scene.__init__(self, *args, **kwargs) - self.radius = RADIUS - self.circle = Circle(density = CIRCLE_DENSITY).scale(self.radius) - self.points = [ - (self.radius * np.cos(angle), self.radius * np.sin(angle), 0) - for angle in radians - ] - self.dots = [Dot(point) for point in self.points] - self.lines = [Line(p1, p2) for p1, p2 in it.combinations(self.points, 2)] - self.add(self.circle, *self.dots + self.lines) - -class GraphScene(Scene): - #Note, the placement of vertices in this is pretty hard coded, be - #warned if you want to change it. - def __init__(self, graph, *args, **kwargs): - Scene.__init__(self, *args, **kwargs) - #See CUBE_GRAPH above for format of graph - self.graph = graph - self.points = map(np.array, graph["vertices"]) - self.vertices = self.dots = [Dot(p) for p in self.points] - self.edges = [ - Line(self.points[i], self.points[j]) - for i, j in graph["edges"] - ] - self.add(*self.dots + self.edges) - - def generate_regions(self): - regions = [ - region_from_line_boundary(*[ - [ - self.points[rc[i]], - self.points[rc[(i+1)%len(rc)]] - ] - for i in range(len(rc)) - ]) - for rc in self.graph["region_cycles"] - ] - regions[-1].complement()#Outer region painted outwardly... - self.regions = regions - -################################################## - def count_lines(*radians): #TODO, Count things explicitly? sc = CircleScene(radians) @@ -378,7 +330,7 @@ def quadruplets_to_intersections(*radians): )) # sc.remove(arrows) - name = "QuadrupletsToIntersections" + len(radians) + name = "QuadrupletsToIntersections%d"%len(radians) sc.write_to_movie(movie_prefix + name) def defining_graph(graph): @@ -447,7 +399,7 @@ def eulers_formula(graph): ]) for mob in form.values(): mob.shift((0, SPACE_HEIGHT-1.5, 0)) - formula = CompoundMobject(*form.values()) + formula = CompoundMobject(*[form[k] for k in form.keys() if k != "=2"]) new_form = dict([ (key, deepcopy(mob).shift((0, -0.7, 0))) for key, mob in zip(form.keys(), form.values()) @@ -486,6 +438,81 @@ def eulers_formula(graph): name = "EulersFormula" + graph["name"] gs.write_to_movie(movie_prefix + name) +def apply_euler_to_moser(*radians): + cs = CircleScene(radians) + cs.remove(cs.n_equals) + n_equals, intersection_count = tex_mobjects([ + r"&n = %d\\"%len(radians), + r"&{%d \choose 4} = %d"%(len(radians), choose(len(radians), 4)) + ]) + shift_val = cs.n_equals.get_center() - n_equals.get_center() + for mob in n_equals, intersection_count: + mob.shift(shift_val) + cs.add(n_equals) + yellow_dots = [d.highlight("yellow") for d in deepcopy(cs.dots)] + yellow_lines = [l.highlight("yellow") for l in deepcopy(cs.lines)] + cs.animate(*[ + ShowCreation(dot) for dot in yellow_dots + ], run_time = 1.0) + cs.dither() + cs.remove(*yellow_dots) + cs.animate(*[ + ShowCreation(line) for line in yellow_lines + ], run_time = 1.0) + cs.dither() + cs.remove(yellow_lines) + cannot_intersect = text_mobject(r""" + Euler's formula does not apply to \\ + graphs whose edges intersect! + """ + ) + cannot_intersect.center() + for mob in cs.mobjects: + mob.fade(0.3) + cs.add(cannot_intersect) + cs.dither() + cs.remove(cannot_intersect) + for mob in cs.mobjects: + mob.fade(1/0.3) + cs.generate_intersection_dots() + cs.animate(FadeIn(intersection_count), *[ + ShowCreation(dot) for dot in cs.intersection_dots + ]) + + + name = "ApplyEulerToMoser%d"%len(radians) + cs.write_to_movie(movie_prefix + name) + +def show_moser_graph_lines(*radians): + radians = list(set(map(lambda x : x%(2*np.pi), radians))) + radians.sort() + + cs = CircleScene(radians) + cs.chop_lines_at_intersection_points() + cs.add(*cs.intersection_dots) + small_lines = [ + deepcopy(line).scale_in_place(0.5) + for line in cs.lines + ] + cs.animate(*[ + Transform(line, small_line, run_time = 3.0) + for line, small_line in zip(cs.lines, small_lines) + ]) + cs.count(cs.lines, color = "yellow", + run_time = 9.0, num_offset = (0, 0, 0)) + cs.dither() + cs.remove(cs.number) + cs.chop_circle_at_points() + cs.animate(*[ + Transform(p, sp, run_time = 3.0) + for p, sp in zip(cs.circle_pieces, cs.smaller_circle_pieces) + ]) + cs.count(cs.circle_pieces, color = "yellow", + run_time = 2.0, num_offset = (0, 0, 0)) + name = "ShowMoserGraphLines%d"%len(radians) + cs.write_to_movie(movie_prefix + name) + + ################################################## @@ -503,13 +530,14 @@ if __name__ == '__main__': # illustrate_n_choose_k(6, 4) # intersection_point_correspondances(radians, range(0, 7, 2)) # lines_intersect_outside(radians, [2, 4, 5, 6]) - quadruplets_to_intersections(*radians[:6]) + # quadruplets_to_intersections(*radians[:6]) # defining_graph(SAMPLE_GRAPH) # doubled_edges(CUBE_GRAPH) # eulers_formula(CUBE_GRAPH) # eulers_formula(SAMPLE_GRAPH) # eulers_formula(OCTOHEDRON_GRAPH) - + # apply_euler_to_moser(*radians) + show_moser_graph_lines(*radians[:6]) diff --git a/moser/moser_helpers.py b/moser/moser_helpers.py index c3fad5d0..2bc0fafe 100644 --- a/moser/moser_helpers.py +++ b/moser/moser_helpers.py @@ -4,6 +4,125 @@ import itertools as it from constants import * from image_mobject import * +from scene import Scene + +RADIUS = SPACE_HEIGHT - 0.1 +CIRCLE_DENSITY = DEFAULT_POINT_DENSITY_1D*RADIUS + +############################################ + +class CircleScene(Scene): + def __init__(self, radians, *args, **kwargs): + Scene.__init__(self, *args, **kwargs) + self.radius = RADIUS + self.circle = Circle(density = CIRCLE_DENSITY).scale(self.radius) + self.points = [ + (self.radius * np.cos(angle), self.radius * np.sin(angle), 0) + for angle in radians + ] + self.dots = [Dot(point) for point in self.points] + self.lines = [Line(p1, p2) for p1, p2 in it.combinations(self.points, 2)] + self.n_equals = tex_mobject( + "n=%d"%len(radians), + size = r"\small" + ).shift((-SPACE_WIDTH+1, SPACE_HEIGHT-1.5, 0)) + self.add(self.circle, self.n_equals, *self.dots + self.lines) + + def generate_intersection_dots(self): + """ + Generates and adds attributes intersection_points and + intersection_dots, but does not yet add them to the scene + """ + self.intersection_points = [ + intersection((p[0], p[2]), (p[1], p[3])) + for p in it.combinations(self.points, 4) + ] + self.intersection_dots = [ + Dot(point) for point in self.intersection_points + ] + + def chop_lines_at_intersection_points(self): + if not hasattr(self, "intersection_dots"): + self.generate_intersection_dots() + self.remove(*self.lines) + self.lines = [] + for point_pair in it.combinations(self.points, 2): + int_points = filter( + lambda p : is_on_line(p, *point_pair), + self.intersection_points + ) + points = list(point_pair) + int_points + points = map(lambda p : (p[0], p[1], 0), points) + points.sort(cmp = lambda x,y: cmp(x[0], y[0])) + self.lines += [ + Line(points[i], points[i+1]) + for i in range(len(points)-1) + ] + self.add(*self.lines) + + def chop_circle_at_points(self): + self.remove(self.circle) + self.circle_pieces = [] + self.smaller_circle_pieces = [] + for i in range(len(self.points)): + pp = self.points[i], self.points[(i+1)%len(self.points)] + transform = np.array([ + [pp[0][0], pp[1][0], 0], + [pp[0][1], pp[1][1], 0], + [0, 0, 1] + ]) + circle = deepcopy(self.circle) + smaller_circle = deepcopy(self.circle) + for c in circle, smaller_circle: + c.points = np.dot( + c.points, + np.transpose(np.linalg.inv(transform)) + ) + c.filter_out( + lambda p : p[0] < 0 or p[1] < 0 + ) + if c == smaller_circle: + c.filter_out( + lambda p : p[0] > 4*p[1] or p[1] > 4*p[0] + ) + c.points = np.dot( + c.points, + np.transpose(transform) + ) + self.circle_pieces.append(circle) + self.smaller_circle_pieces.append(smaller_circle) + self.add(*self.circle_pieces) + +class GraphScene(Scene): + #Note, the placement of vertices in this is pretty hard coded, be + #warned if you want to change it. + def __init__(self, graph, *args, **kwargs): + Scene.__init__(self, *args, **kwargs) + #See CUBE_GRAPH above for format of graph + self.graph = graph + self.points = map(np.array, graph["vertices"]) + self.vertices = self.dots = [Dot(p) for p in self.points] + self.edges = [ + Line(self.points[i], self.points[j]) + for i, j in graph["edges"] + ] + self.add(*self.dots + self.edges) + + def generate_regions(self): + regions = [ + region_from_line_boundary(*[ + [ + self.points[rc[i]], + self.points[rc[(i+1)%len(rc)]] + ] + for i in range(len(rc)) + ]) + for rc in self.graph["region_cycles"] + ] + regions[-1].complement()#Outer region painted outwardly... + self.regions = regions + +################################################## def choose(n, r): if n < r: return 0 @@ -15,6 +134,16 @@ def choose(n, r): def moser_function(n): return choose(n, 4) + choose(n, 2) + 1 +def is_on_line(p0, p1, p2, threshold = 0.01): + """ + Returns true of p0 is on the line between p1 and p2 + """ + p0, p1, p2 = map(lambda tup : np.array(tup[:2]), [p0, p1, p2]) + p1 -= p0 + p2 -= p0 + return abs((p1[0] / p1[1]) - (p2[0] / p2[1])) < threshold + + def intersection(line1, line2): """ A "line" should come in the form [(x0, y0), (x1, y1)] for two diff --git a/tex_utils.py b/tex_utils.py index 9d18365a..9ea480fe 100644 --- a/tex_utils.py +++ b/tex_utils.py @@ -3,7 +3,9 @@ import itertools as it from PIL import Image from constants import * -def tex_to_image(expression, size = "\HUGE"): +def tex_to_image(expression, + size = "\HUGE", + template_tex_file = TEMPLATE_TEX_FILE): """ Returns list of images for correpsonding with a list of expressions """ @@ -27,7 +29,7 @@ def tex_to_image(expression, size = "\HUGE"): "at size %s to "%(size), filestem, ]) - with open(TEMPLATE_TEX_FILE, "r") as infile: + with open(template_tex_file, "r") as infile: body = infile.read() body = body.replace(SIZE_TO_REPLACE, size) body = body.replace(TEX_TEXT_TO_REPLACE, expression)