From fc395d14e1482805eb44ecfd6d476dd1e82831a5 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 19 Jun 2015 08:31:02 -0700 Subject: [PATCH] Nearly done with ecf --- animation/animation.py | 26 +- animation/simple_animations.py | 34 +- animation/transform.py | 26 +- constants.py | 17 +- displayer.py | 7 +- helpers.py | 4 + mobject/creatures.py | 68 ++- mobject/image_mobject.py | 13 +- mobject/mobject.py | 38 +- mobject/simple_mobjects.py | 28 +- region.py | 24 +- sample_script.py | 29 +- scene/graphs.py | 283 ++++++++++- scene/pascals_triangle.py | 88 ++++ scene/scene.py | 20 +- scene/sub_scenes.py | 84 +--- script_wrapper.py | 8 +- scripts/ecf_graph_scenes.py | 875 ++++++++++++++++++++++++++++++++- 18 files changed, 1487 insertions(+), 185 deletions(-) create mode 100644 scene/pascals_triangle.py diff --git a/animation/animation.py b/animation/animation.py index b54116b8..b7d77eed 100644 --- a/animation/animation.py +++ b/animation/animation.py @@ -108,31 +108,7 @@ class Animation(object): pass def clean_up(self): - pass - - -#Fuck this is cool! -# class TransformAnimations(Transform): -# def __init__(self, start_anim, end_anim, -# alpha_func = squish_alpha_func(high_inflection_0_to_1), -# **kwargs): -# self.start_anim, self.end_anim = start_anim, end_anim -# Transform.__init__( -# self, -# start_anim.mobject, -# end_anim.mobject, -# run_time = max(start_anim.run_time, end_anim.run_time), -# alpha_func = alpha_func, -# **kwargs -# ) -# #Rewire starting and ending mobjects -# start_anim.mobject = self.starting_mobject -# end_anim.mobject = self.ending_mobject - -# def update(self, alpha): -# self.start_anim.update(alpha) -# self.end_anim.update(alpha) -# Transform.update(self, alpha) + self.update(1) diff --git a/animation/simple_animations.py b/animation/simple_animations.py index 2995eb64..e2ac650b 100644 --- a/animation/simple_animations.py +++ b/animation/simple_animations.py @@ -1,7 +1,10 @@ import numpy as np import itertools as it +from copy import deepcopy from animation import Animation +from transform import Transform +from mobject import * from constants import * from helpers import * @@ -129,7 +132,36 @@ class ComplexHomotopy(Homotopy): to_cammel_case(complex_homotopy.__name__) - +class WalkPiCreature(Animation): + def __init__(self, pi_creature, destination, *args, **kwargs): + self.final = deepcopy(pi_creature).move_to(destination) + self.middle = pi_creature.get_step_intermediate(self.final) + Animation.__init__(self, pi_creature, *args, **kwargs) + + def update_mobject(self, alpha): + if alpha < 0.5: + Mobject.interpolate( + self.starting_mobject, + self.middle, + self.mobject, + 2*alpha + ) + else: + Mobject.interpolate( + self.middle, + self.final, + self.mobject, + 2*alpha - 1 + ) + + + +###### Something different ############### +def pi_creature_step(scene, pi_creature, destination): + final = deepcopy(pi_creature).move_to(destination) + intermediate = pi_creature.get_step_intermediate(final) + scene.animate(Transform(pi_creature, intermediate)) + scene.animate(Transform(pi_creature, final)) diff --git a/animation/transform.py b/animation/transform.py index 84ef506c..3a28b217 100644 --- a/animation/transform.py +++ b/animation/transform.py @@ -12,7 +12,7 @@ from helpers import * class Transform(Animation): def __init__(self, mobject1, mobject2, run_time = DEFAULT_TRANSFORM_RUN_TIME, - black_out_extra_points = True, + black_out_extra_points = False, *args, **kwargs): count1, count2 = mobject1.get_num_points(), mobject2.get_num_points() if count2 == 0: @@ -45,6 +45,7 @@ class Transform(Animation): ) def clean_up(self): + Animation.clean_up(self) if hasattr(self, "non_redundant_m2_indices"): #Reduce mobject (which has become identical to mobject2), as #well as mobject2 itself @@ -149,7 +150,28 @@ class ComplexFunction(ApplyFunction): #Todo, abstract away function naming' - +#Fuck this is cool! +class TransformAnimations(Transform): + def __init__(self, start_anim, end_anim, + alpha_func = squish_alpha_func(high_inflection_0_to_1), + **kwargs): + self.start_anim, self.end_anim = start_anim, end_anim + Transform.__init__( + self, + start_anim.mobject, + end_anim.mobject, + run_time = max(start_anim.run_time, end_anim.run_time), + alpha_func = alpha_func, + **kwargs + ) + #Rewire starting and ending mobjects + start_anim.mobject = self.starting_mobject + end_anim.mobject = self.ending_mobject + + def update(self, alpha): + self.start_anim.update(alpha) + self.end_anim.update(alpha) + Transform.update(self, alpha) diff --git a/constants.py b/constants.py index dbc62d0b..c46246f2 100644 --- a/constants.py +++ b/constants.py @@ -1,4 +1,5 @@ import os +import numpy as np GENERALLY_BUFF_POINTS = True @@ -22,18 +23,26 @@ DEFAULT_POINT_DENSITY_1D = 150 #TODO, Make sure these are not needd DEFAULT_HEIGHT = PRODUCTION_QUALITY_DISPLAY_CONFIG["height"] DEFAULT_WIDTH = PRODUCTION_QUALITY_DISPLAY_CONFIG["width"] +SPACE_HEIGHT = 4.0 +SPACE_WIDTH = SPACE_HEIGHT * DEFAULT_WIDTH / DEFAULT_HEIGHT + + #All in seconds DEFAULT_FRAME_DURATION = 0.04 DEFAULT_ANIMATION_RUN_TIME = 3.0 DEFAULT_TRANSFORM_RUN_TIME = 1.0 DEFAULT_DITHER_TIME = 1.0 - DEFAULT_NUM_STARS = 1000 -SPACE_HEIGHT = 4.0 -SPACE_WIDTH = SPACE_HEIGHT * DEFAULT_WIDTH / DEFAULT_HEIGHT -ORIGIN = (0, 0, 0) + +ORIGIN = np.array(( 0, 0, 0)) +UP = np.array(( 0, 1, 0)) +DOWN = np.array(( 0,-1, 0)) +RIGHT = np.array(( 1, 0, 0)) +LEFT = np.array((-1, 0, 0)) +IN = np.array(( 0, 0, 1)) +OUT = np.array(( 0, 0,-1)) THIS_DIR = os.path.dirname(os.path.realpath(__file__)) FILE_DIR = os.path.join(THIS_DIR, "files") diff --git a/displayer.py b/displayer.py index c75a15d2..d9206a45 100644 --- a/displayer.py +++ b/displayer.py @@ -10,7 +10,7 @@ import progressbar from mobject import * from constants import * -def get_pixels(image_array): +def get_pixels(image_array): #TODO, FIX WIDTH/HEIGHT PROBLEM HERE if image_array is None: return np.zeros( (DEFAULT_HEIGHT, DEFAULT_WIDTH, 3), @@ -48,6 +48,11 @@ def paint_mobject(mobject, image_array = None): rgbs = np.array(mobject.rgbs) #Flips y-axis points[:,1] *= -1 + #Removes points out of space + points = points[ + (abs(points[:,0]) < SPACE_WIDTH) & + (abs(points[:,1]) < SPACE_HEIGHT) + ] #Map points to pixel space, then create pixel array first in terms #of its flattened version try: diff --git a/helpers.py b/helpers.py index 8c6ce128..d12560ad 100644 --- a/helpers.py +++ b/helpers.py @@ -9,6 +9,10 @@ import operator as op from constants import * +def center_of_mass(points): + points = [np.array(point).astype("float") for point in points] + return sum(points) / len(points) + def choose(n, r): if n < r: return 0 if r == 0: return 1 diff --git a/mobject/creatures.py b/mobject/creatures.py index 840ad70d..6c120b88 100644 --- a/mobject/creatures.py +++ b/mobject/creatures.py @@ -7,9 +7,10 @@ from mobject import * from simple_mobjects import * -class PiCreature(CompoundMobject): +class PiCreature(Mobject): DEFAULT_COLOR = "blue" def __init__(self, color = DEFAULT_COLOR, **kwargs): + Mobject.__init__(self, color = color, **kwargs) scale_val = 0.5 mouth_to_eyes_distance = 0.25 part_names = [ @@ -24,7 +25,7 @@ class PiCreature(CompoundMobject): white_part_names = ['left_eye', 'right_eye', 'mouth'] directory = os.path.join(IMAGE_DIR, "PiCreature") - self.parts = [] + parts = [] self.white_parts = [] for part_name in part_names: path = os.path.join(directory, "pi_creature_"+part_name) @@ -36,29 +37,56 @@ class PiCreature(CompoundMobject): else: mob.highlight(color) setattr(self, part_name, mob) - self.parts.append(mob) + parts.append(mob) self.mouth.center().shift( self.left_eye.get_center()/2 + self.right_eye.get_center()/2 - (0, mouth_to_eyes_distance, 0) ) - self.reload_parts() - def reload_parts(self): - CompoundMobject.__init__(self, *self.parts) - return self + for part in parts: + self.add(part) + self.parts = parts + + def rewire_part_attributes(self, self_from_parts = False): + if self_from_parts: + total_num_points = sum(map(Mobject.get_num_points, self.parts)) + self.points = np.zeros((total_num_points, Mobject.DIM)) + self.rgbs = np.zeros((total_num_points, Mobject.DIM)) + curr = 0 + for part in self.parts: + n_points = part.get_num_points() + if self_from_parts: + self.points[curr:curr+n_points,:] = part.points + self.rgbs[curr:curr+n_points,:] = part.rgbs + else: + part.points = self.points[curr:curr+n_points,:] + part.rgbs = self.rgbs[curr:curr+n_points,:] + curr += n_points + def highlight(self, color): for part in set(self.parts).difference(self.white_parts): part.highlight(color) - return self.reload_parts() + self.rewire_part_attributes(self_from_parts = True) + return self + + def move_to(self, destination): + bottom = np.array(( + np.mean(self.points[:,0]), + min(self.points[:,1]), + 0 + )) + self.shift(destination-bottom) + return self def give_frown(self): center = self.mouth.get_center() self.mouth.center() self.mouth.apply_function(lambda (x, y, z) : (x, -y, z)) self.mouth.shift(center) - return self.reload_parts() + self.rewire_part_attributes(self_from_parts = True) + return self def give_straight_face(self): center = self.mouth.get_center() @@ -69,7 +97,21 @@ class PiCreature(CompoundMobject): self.parts[self.parts.index(self.mouth)] = new_mouth self.white_parts[self.white_parts.index(self.mouth)] = new_mouth self.mouth = new_mouth - return self.reload_parts() + self.rewire_part_attributes(self_from_parts = True) + return self + + def get_step_intermediate(self, pi_creature): + vect = pi_creature.get_center() - self.get_center() + result = deepcopy(self).shift(vect / 2.0) + result.rewire_part_attributes() + if vect[0] < 0: + result.right_leg.wag(-vect/2.0, DOWN) + result.left_leg.wag(vect/2.0, DOWN) + else: + result.left_leg.wag(-vect/2.0, DOWN) + result.right_leg.wag(vect/2.0, DOWN) + return result + class Randolph(PiCreature): pass #Nothing more than an alternative name @@ -79,6 +121,12 @@ class Mortimer(PiCreature): PiCreature.__init__(self, *args, **kwargs) self.highlight(DARK_BROWN) self.give_straight_face() + self.rotate(np.pi, UP) + self.rewire_part_attributes() + + + + diff --git a/mobject/image_mobject.py b/mobject/image_mobject.py index 763db82b..8022c67e 100644 --- a/mobject/image_mobject.py +++ b/mobject/image_mobject.py @@ -87,20 +87,19 @@ class VideoIcon(ImageMobject): self.scale(0.3) def text_mobject(text, size = "\\Large"): - image = tex_to_image(text, size, TEMPLATE_TEXT_FILE) - assert(not isinstance(image, list)) - return ImageMobject(image).center() + return tex_mobjects(text, size, TEMPLATE_TEXT_FILE) #Purely redundant function to make singulars and plurals sensible def tex_mobject(expression, size = "\\Huge"): return tex_mobjects(expression, size) -def tex_mobjects(expression, size = "\\Huge"): - images = tex_to_image(expression, size) +def tex_mobjects(expression, + size = "\\Huge", + template_tex_file = TEMPLATE_TEX_FILE): + images = tex_to_image(expression, size, template_tex_file) if isinstance(images, list): #TODO, is checking listiness really the best here? - result = [ImageMobject(im) for im in images] - return CompoundMobject(*result).center().split() + return CompoundMobject(*map(ImageMobject, images)).center().split() else: return ImageMobject(images).center() diff --git a/mobject/mobject.py b/mobject/mobject.py index b654e83b..d105439e 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -79,7 +79,7 @@ class Mobject(object): self.unit_normals = np.dot(self.unit_normals, t_rotation_matrix) return self - def rotate_in_place(self, angle, axis = [0, 0, 1]): + def rotate_in_place(self, angle, axis = (0, 0, 1)): center = self.get_center() self.shift(-center) self.rotate(angle, axis) @@ -95,7 +95,7 @@ class Mobject(object): self.points += v return self - def wag(self, wag_direction = [0, 1, 0], wag_axis = [-1, 0, 0]): + def wag(self, wag_direction = (0, 1, 0), wag_axis = (-1, 0, 0)): alphas = np.dot(self.points, np.transpose(wag_axis)) alphas -= min(alphas) alphas /= max(alphas) @@ -109,6 +109,31 @@ class Mobject(object): self.shift(-self.get_center()) return self + #To wrapper functions for better naming + def to_corner(self, corner = (-1, 1, 0), buff = 0.5): + return self.align_on_border(corner, buff) + + def to_edge(self, edge = (-1, 0, 0), buff = 0.5): + return self.align_on_border(edge, buff) + + def align_on_border(self, direction, buff = 0.5): + """ + Direction just needs to be a vector pointing towards side or + corner in the 2d plane. + """ + shift_val = [0, 0, 0] + space_dim = (SPACE_WIDTH, SPACE_HEIGHT) + for i in [0, 1]: + if direction[i] == 0: + continue + elif direction[i] > 0: + shift_val[i] = space_dim[i]-buff-max(self.points[:,i]) + else: + shift_val[i] = -space_dim[i]+buff-min(self.points[:,i]) + self.shift(shift_val) + return self + + def scale(self, scale_factor): self.points *= scale_factor return self @@ -154,8 +179,8 @@ class Mobject(object): self.rgbs[to_change, :] += Color(color).get_rgb() return self - def fade(self, amount = 0.5): - self.rgbs *= amount + def fade(self, brightness = 0.5): + self.rgbs *= brightness return self def filter_out(self, condition): @@ -178,6 +203,11 @@ class Mobject(object): def get_height(self): return np.max(self.points[:, 1]) - np.min(self.points[:, 1]) + def get_color(self): + color = Color() + color.set_rgb(self.rgbs[0, :]) + return color + ### Stuff subclasses should deal with ### def should_buffer_points(self): # potentially changed in subclasses diff --git a/mobject/simple_mobjects.py b/mobject/simple_mobjects.py index 32c9e625..619615f8 100644 --- a/mobject/simple_mobjects.py +++ b/mobject/simple_mobjects.py @@ -15,12 +15,16 @@ 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, + tail = None, 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) + if tail == None: + self.direction = np.array(direction) / np.linalg.norm(direction) + self.length = length + else: + self.direction = self.point - tail + self.length = np.linalg.norm(self.direction) self.normal = np.array(normal) - self.length = length self.tip_length = tip_length Mobject1D.__init__(self, *args, **kwargs) @@ -103,17 +107,21 @@ class Line(Mobject1D): return rise/run class CurvedLine(Line): + def __init__(self, start, end, via = None, *args, **kwargs): + if via == None: + via = rotate_vector( + end - start, + np.pi/3, [0,0,1] + ) + start + self.via = via + Line.__init__(self, start, end, *args, **kwargs) + 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 + 4*(0.25-t*(1-t))*(t*self.end + (1-t)*self.start) + + 4*t*(1-t)*self.via for t in np.arange(0, 1, self.epsilon) ]) - self.ep = equidistant_point class Circle(Mobject1D): DEFAULT_COLOR = "red" diff --git a/region.py b/region.py index 45cfc772..6ed9b85c 100644 --- a/region.py +++ b/region.py @@ -10,7 +10,7 @@ import displayer as disp class Region(object): def __init__(self, condition = None, - shape = (DEFAULT_HEIGHT, DEFAULT_WIDTH) + shape = None, ): """ Condition must be a function which takes in two real @@ -19,8 +19,10 @@ class Region(object): a function from R^2 to {True, False}, but & and | must be used in place of "and" and "or" """ - #TODO, maybe I want this to be flexible to resizing - self.shape = shape + if shape == None: + self.shape = (DEFAULT_HEIGHT, DEFAULT_WIDTH) + else: + self.shape = shape # self.condition = condition (h, w) = self.shape scalar = 2*SPACE_HEIGHT / h @@ -76,13 +78,13 @@ class HalfPlane(Region): return (x1 - x0)*(y - y0) > (y1 - y0)*(x - x0) Region.__init__(self, condition, *args, **kwargs) -def region_from_line_boundary(*lines): - reg = Region() +def region_from_line_boundary(*lines, **kwargs): + reg = Region(**kwargs) for line in lines: - reg.intersect(HalfPlane(line)) + reg.intersect(HalfPlane(line, **kwargs)) return reg -def plane_partition(*lines): +def plane_partition(*lines, **kwargs): """ A 'line' is a pair of points [(x0, y0,...), (x1, y1,...)] @@ -90,11 +92,11 @@ def plane_partition(*lines): these lines """ result = [] - half_planes = [HalfPlane(line) for line in lines] + half_planes = [HalfPlane(line, **kwargs) for line in lines] complements = [deepcopy(hp).complement() for hp in half_planes] num_lines = len(lines) for bool_list in it.product(*[[True, False]]*num_lines): - reg = Region() + reg = Region(**kwargs) for i in range(num_lines): if bool_list[i]: reg.intersect(half_planes[i]) @@ -104,7 +106,7 @@ def plane_partition(*lines): result.append(reg) return result -def plane_partition_from_points(*points): +def plane_partition_from_points(*points, **kwargs): """ Returns list of regions cut out by the complete graph with points from the argument as vertices. @@ -112,7 +114,7 @@ def plane_partition_from_points(*points): Each point comes in the form (x, y) """ lines = [[p1, p2] for (p1, p2) in it.combinations(points, 2)] - return plane_partition(*lines) + return plane_partition(*lines, **kwargs) diff --git a/sample_script.py b/sample_script.py index 1bf9bd9d..e860fec2 100644 --- a/sample_script.py +++ b/sample_script.py @@ -2,33 +2,32 @@ import numpy as np import itertools as it -import operator as op from copy import deepcopy -from random import random, randint import sys -import inspect from animation import * from mobject import * from constants import * from region import * -from scene import Scene +from scene import Scene, GraphScene from script_wrapper import command_line_create_scene -class SampleScene(Scene): +class SampleScene(GraphScene): def construct(self): - mob = Mobject() - circle9 = Circle().repeat(9).scale(3) - Mobject.interpolate(Circle().scale(3), circle9, mob, 0.8) - self.animate(Transform( - mob, - circle9, - run_time = 3.0, - alpha_func = there_and_back, - )) + GraphScene.construct(self) + self.generate_regions() + self.generate_dual_graph() + self.generate_spanning_tree() + self.add(self.spanning_tree) + for count in range(len(self.regions)): + self.add(tex_mobject(str(count)).shift(self.dual_points[count])) + for count in range(len(self.edges)): + self.add(tex_mobject(str(count)).shift(self.edges[count].get_center())) + + if __name__ == "__main__": - command_line_create_scene(sys.argv[1:]) \ No newline at end of file + command_line_create_scene() \ No newline at end of file diff --git a/scene/graphs.py b/scene/graphs.py index 966f5fd7..8538ed80 100644 --- a/scene/graphs.py +++ b/scene/graphs.py @@ -1,5 +1,15 @@ import itertools as it import numpy as np +import operator as op +from random import random + +from scene import Scene + +from mobject import * +from animation import * +from region import * +from constants import * +from helpers import * CUBE_GRAPH = { "name" : "CubeGraph", @@ -78,7 +88,7 @@ SAMPLE_GRAPH = { (0, 5, 1), (1, 5, 6, 7), (1, 7, 8, 3), - (4, 5, 6, 7, 8), + (4, 5, 6, 7, 8, 3, 2), ] } @@ -120,3 +130,274 @@ OCTOHEDRON_GRAPH = { (3, 4, 5), ] } + +def complete_graph(n, radius = 3): + return { + "name" : "Complete%d"%n, + "vertices" : [ + (radius*np.cos(theta), radius*np.sin(theta), 0) + for x in range(n) + for theta in [2*np.pi*x / n] + ], + "edges" : it.combinations(range(n), 2) + } + +class GraphScene(Scene): + args_list = [ + (CUBE_GRAPH,), + (SAMPLE_GRAPH,), + (OCTOHEDRON_GRAPH,), + ] + @staticmethod + def args_to_string(*args): + return args[0]["name"] + + def __init__(self, graph, *args, **kwargs): + #See CUBE_GRAPH above for format of graph + self.graph = graph + Scene.__init__(self, *args, **kwargs) + + def construct(self): + self.points = map(np.array, self.graph["vertices"]) + self.vertices = self.dots = [Dot(p) for p in self.points] + self.edges = self.lines = [ + Line(self.points[i], self.points[j]) + for i, j in self.graph["edges"] + ] + self.add(*self.dots + self.edges) + + def generate_regions(self): + regions = [ + self.region_from_cycle(cycle) + for cycle in self.graph["region_cycles"] + ] + regions[-1].complement()#Outer region painted outwardly... + self.regions = regions + + def region_from_cycle(self, cycle): + point_pairs = [ + [ + self.points[cycle[i]], + self.points[cycle[(i+1)%len(cycle)]] + ] + for i in range(len(cycle)) + ] + return region_from_line_boundary( + *point_pairs, shape = self.shape + ) + + def draw_vertices(self, **kwargs): + self.clear() + self.animate(ShowCreation(CompoundMobject(*self.vertices), **kwargs)) + + def draw_edges(self): + self.animate(*[ + ShowCreation(edge, run_time = 1.0) + for edge in self.edges + ]) + + def accent_vertices(self, **kwargs): + self.remove(*self.vertices) + start = CompoundMobject(*self.vertices) + end = CompoundMobject(*[ + Dot(point, radius = 3*Dot.DEFAULT_RADIUS, color = "lightgreen") + for point in self.points + ]) + self.animate(Transform( + start, end, alpha_func = there_and_back, + **kwargs + )) + self.remove(start) + self.add(*self.vertices) + + + def replace_vertices_with(self, mobject): + mobject.center() + diameter = max(mobject.get_height(), mobject.get_width()) + self.animate(*[ + SemiCircleTransform( + vertex, + deepcopy(mobject).shift(vertex.get_center()) + ) + for vertex in self.vertices + ] + [ + ApplyMethod( + edge.scale_in_place, + (edge.get_length() - diameter) / edge.get_length() + ) + for edge in self.edges + ]) + + def annotate_edges(self, mobject, fade_in = True, **kwargs): + angles = map(np.arctan, map(Line.get_slope, self.edges)) + self.edge_annotations = [ + deepcopy(mobject).rotate(angle).shift(edge.get_center()) + for angle, edge in zip(angles, self.edges) + ] + if fade_in: + self.animate(*[ + FadeIn(ann, **kwargs) + for ann in self.edge_annotations + ]) + + def trace_cycle(self, cycle = None, color = "yellow", run_time = 2.0): + if cycle == None: + cycle = self.graph["region_cycles"][0] + time_per_edge = run_time / len(cycle) + next_in_cycle = it.cycle(cycle) + next_in_cycle.next()#jump one ahead + self.traced_cycle = CompoundMobject(*[ + Line(self.points[i], self.points[j]).highlight(color) + for i, j in zip(cycle, next_in_cycle) + ]) + self.animate( + ShowCreation(self.traced_cycle), + run_time = run_time + ) + + def generate_spanning_tree(self, root = 0, color = "yellow"): + self.spanning_tree_root = 0 + pairs = deepcopy(self.graph["edges"]) + pairs += [tuple(reversed(pair)) for pair in pairs] + self.spanning_tree_index_pairs = [] + curr = root + spanned_vertices = set([curr]) + to_check = set([curr]) + while len(to_check) > 0: + curr = to_check.pop() + for pair in pairs: + if pair[0] == curr and pair[1] not in spanned_vertices: + self.spanning_tree_index_pairs.append(pair) + spanned_vertices.add(pair[1]) + to_check.add(pair[1]) + self.spanning_tree = CompoundMobject(*[ + Line( + self.points[pair[0]], + self.points[pair[1]] + ).highlight(color) + for pair in self.spanning_tree_index_pairs + ]) + + def generate_treeified_spanning_tree(self): + bottom = -SPACE_HEIGHT + 1 + x_sep = 1 + y_sep = 2 + if not hasattr(self, "spanning_tree"): + self.generate_spanning_tree() + root = self.spanning_tree_root + color = self.spanning_tree.get_color() + indices = range(len(self.points)) + #Build dicts + parent_of = dict([ + tuple(reversed(pair)) + for pair in self.spanning_tree_index_pairs + ]) + children_of = dict([(index, []) for index in indices]) + for child in parent_of: + children_of[parent_of[child]].append(child) + + x_coord_of = {root : 0} + y_coord_of = {root : bottom} + #width to allocate to a given node, computed as + #the maxium number of decendents in a single generation, + #minus 1, multiplied by x_sep + width_of = {} + for index in indices: + next_generation = children_of[index] + curr_max = max(1, len(next_generation)) + while next_generation != []: + next_generation = reduce(op.add, [ + children_of[node] + for node in next_generation + ]) + curr_max = max(curr_max, len(next_generation)) + width_of[index] = x_sep * (curr_max - 1) + to_process = [root] + while to_process != []: + index = to_process.pop() + if index not in y_coord_of: + y_coord_of[index] = y_sep + y_coord_of[parent_of[index]] + children = children_of[index] + left_hand = x_coord_of[index]-width_of[index]/2.0 + for child in children: + x_coord_of[child] = left_hand + width_of[child]/2.0 + left_hand += width_of[child] + x_sep + to_process += children + + new_points = [ + np.array([ + x_coord_of[index], + y_coord_of[index], + 0 + ]) + for index in indices + ] + self.treeified_spanning_tree = CompoundMobject(*[ + Line(new_points[i], new_points[j]).highlight(color) + for i, j in self.spanning_tree_index_pairs + ]) + + def generate_dual_graph(self): + point_at_infinity = np.array([np.inf]*3) + cycles = self.graph["region_cycles"] + self.dual_points = [ + center_of_mass([ + self.points[index] + for index in cycle + ]) + for cycle in cycles + ] + self.dual_vertices = [ + Dot(point).highlight("green") + for point in self.dual_points + ] + self.dual_vertices[-1] = Circle().scale(SPACE_WIDTH + SPACE_HEIGHT) + self.dual_points[-1] = point_at_infinity + + self.dual_edges = [] + for pair in self.graph["edges"]: + dual_point_pair = [] + for cycle in cycles: + if not (pair[0] in cycle and pair[1] in cycle): + continue + index1, index2 = cycle.index(pair[0]), cycle.index(pair[1]) + if abs(index1 - index2) in [1, len(cycle)-1]: + dual_point_pair.append( + self.dual_points[cycles.index(cycle)] + ) + assert(len(dual_point_pair) == 2) + for i in 0, 1: + if all(dual_point_pair[i] == point_at_infinity): + new_point = np.array(dual_point_pair[1-i]) + vect = center_of_mass([ + self.points[pair[0]], + self.points[pair[1]] + ]) - new_point + new_point += SPACE_WIDTH*vect/np.linalg.norm(vect) + dual_point_pair[i] = new_point + self.dual_edges.append( + Line(*dual_point_pair).highlight() + ) + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scene/pascals_triangle.py b/scene/pascals_triangle.py new file mode 100644 index 00000000..627781c8 --- /dev/null +++ b/scene/pascals_triangle.py @@ -0,0 +1,88 @@ +import numpy as np +import itertools as it + +from scene import Scene + +from mobject import * +from animation import * +from region import * +from constants import * +from helpers import * + + +BIG_N_PASCAL_ROWS = 11 +N_PASCAL_ROWS = 7 +class PascalsTriangleScene(Scene): + args_list = [ + (N_PASCAL_ROWS,), + (BIG_N_PASCAL_ROWS,), + ] + @staticmethod + def args_to_string(*args): + return str(args[0]) + + def __init__(self, nrows, *args, **kwargs): + Scene.__init__(self, *args, **kwargs) + self.nrows = nrows + self.diagram_height = 2*SPACE_HEIGHT - 1 + self.diagram_width = 1.5*SPACE_WIDTH + self.cell_height = self.diagram_height / nrows + self.cell_width = self.diagram_width / nrows + self.portion_to_fill = 0.7 + self.bottom_left = np.array( + (-self.cell_width * nrows / 2.0, -self.cell_height * nrows / 2.0, 0) + ) + num_to_num_mob = {} + self.coords_to_mobs = {} + self.coords = [(n, k) for n in range(nrows) for k in range(n+1)] + for n, k in self.coords: + num = choose(n, k) + center = self.coords_to_center(n, k) + if num not in num_to_num_mob: + num_to_num_mob[num] = tex_mobject(str(num)) + num_mob = deepcopy(num_to_num_mob[num]) + scale_factor = min( + 1, + self.portion_to_fill * self.cell_height / num_mob.get_height(), + self.portion_to_fill * self.cell_width / num_mob.get_width(), + ) + num_mob.center().scale(scale_factor).shift(center) + if n not in self.coords_to_mobs: + self.coords_to_mobs[n] = {} + self.coords_to_mobs[n][k] = num_mob + self.add(*[self.coords_to_mobs[n][k] for n, k in self.coords]) + + def coords_to_center(self, n, k): + return self.bottom_left + ( + self.cell_width * (k+self.nrows/2.0 - n/2.0), + self.cell_height * (self.nrows - n), + 0 + ) + + def generate_n_choose_k_mobs(self): + self.coords_to_n_choose_k = {} + for n, k in self.coords: + nck_mob = tex_mobject(r"{%d \choose %d}"%(n, k)) + scale_factor = min( + 1, + self.portion_to_fill * self.cell_height / nck_mob.get_height(), + self.portion_to_fill * self.cell_width / nck_mob.get_width(), + ) + center = self.coords_to_mobs[n][k].get_center() + nck_mob.center().scale(scale_factor).shift(center) + if n not in self.coords_to_n_choose_k: + self.coords_to_n_choose_k[n] = {} + self.coords_to_n_choose_k[n][k] = nck_mob + + def generate_sea_of_zeros(self): + zero = tex_mobject("0") + self.sea_of_zeros = [] + for n in range(self.nrows): + for a in range((self.nrows - n)/2 + 1): + for k in (n + a + 1, -a -1): + self.coords.append((n, k)) + mob = deepcopy(zero) + mob.shift(self.coords_to_center(n, k)) + self.coords_to_mobs[n][k] = mob + self.add(mob) + diff --git a/scene/scene.py b/scene/scene.py index fd381fb8..a5871096 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -127,6 +127,7 @@ class Scene(object): def count_mobjects( self, mobjects, mode = "highlight", color = "red", + display_numbers = True, num_offset = DEFAULT_COUNT_NUM_OFFSET, run_time = DEFAULT_COUNT_RUN_TIME): """ @@ -146,9 +147,10 @@ class Scene(object): if mode == "highlight": self.add(*mobjects) for mob, num in zip(mobjects, it.count(1)): - num_mob = tex_mobject(str(num)) - num_mob.center().shift(num_offset) - self.add(num_mob) + if display_numbers: + num_mob = tex_mobject(str(num)) + num_mob.center().shift(num_offset) + self.add(num_mob) if mode == "highlight": original_color = mob.color mob.highlight(color) @@ -159,9 +161,11 @@ class Scene(object): if mode == "show": self.add(mob) self.dither(frame_time) - self.remove(num_mob) - self.add(num_mob) - self.number = num_mob + if display_numbers: + self.remove(num_mob) + if display_numbers: + self.add(num_mob) + self.number = num_mob return self def count_regions(self, regions, @@ -212,8 +216,8 @@ class Scene(object): def preview(self): TkSceneRoot(self) - def save_image(self, path): - path = os.path.join(MOVIE_DIR, path) + ".png" + def save_image(self, directory = MOVIE_DIR, name = None): + path = os.path.join(directory, name or str(self)) + ".png" Image.fromarray(self.get_frame()).save(path) # To list possible args that subclasses have diff --git a/scene/sub_scenes.py b/scene/sub_scenes.py index 43698559..45cc3e78 100644 --- a/scene/sub_scenes.py +++ b/scene/sub_scenes.py @@ -10,87 +10,6 @@ from region import * from constants import * from helpers import * -class GraphScene(Scene): - args_list = [ - (CUBE_GRAPH,), - (SAMPLE_GRAPH,), - (OCTOHEDRON_GRAPH,), - ] - @staticmethod - def args_to_string(*args): - return args[0]["name"] - - def __init__(self, graph, *args, **kwargs): - #See CUBE_GRAPH above for format of graph - self.graph = graph - Scene.__init__(self, *args, **kwargs) - - def construct(self): - self.points = map(np.array, self.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 self.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 generate_spanning_tree(self): - pass - - def draw_vertices(self): - self.clear() - self.animate(ShowCreation(CompoundMobject(*self.vertices))) - - def draw_edges(self): - self.animate(*[ - ShowCreation(edge, run_time = 1.0) - for edge in self.edges - ]) - - def replace_vertices_with(self, mobject): - mobject.center() - diameter = max(mobject.get_height(), mobject.get_width()) - self.animate(*[ - SemiCircleTransform( - vertex, - deepcopy(mobject).shift(vertex.get_center()) - ) - for vertex in self.vertices - ] + [ - ApplyMethod( - edge.scale_in_place, - (edge.get_length() - diameter) / edge.get_length() - ) - for edge in self.edges - ]) - - def annotate_edges(self, mobject): - angles = map(np.arctan, map(Line.get_slope, self.edges)) - self.edge_annotations = [ - deepcopy(mobject).rotate(angle).shift(edge.get_center()) - for angle, edge in zip(angles, self.edges) - ] - self.animate(*[ - FadeIn(ann) - for ann in self.edge_annotations - ]) - - - BIG_N_PASCAL_ROWS = 11 N_PASCAL_ROWS = 7 @@ -166,4 +85,5 @@ class PascalsTriangleScene(Scene): mob = deepcopy(zero) mob.shift(self.coords_to_center(n, k)) self.coords_to_mobs[n][k] = mob - self.add(mob) \ No newline at end of file + self.add(mob) + diff --git a/script_wrapper.py b/script_wrapper.py index 48830bcb..c3310d0a 100644 --- a/script_wrapper.py +++ b/script_wrapper.py @@ -94,7 +94,7 @@ def command_line_create_scene(movie_prefix = ""): display_config = LOW_QUALITY_DISPLAY_CONFIG action = "preview" elif opt == '-s': - action = "show_frame" + action = "save_image" if len(args) > 0: scene_string = args[0] if len(args) > 1: @@ -111,8 +111,12 @@ def command_line_create_scene(movie_prefix = ""): scene.write_to_movie(movie_prefix + name) elif action == "preview": scene.preview() - elif action == "show_frame": + elif action == "save_image": scene.show_frame() + path = os.path.join(MOVIE_DIR, movie_prefix, "images") + if not os.path.exists(path): + os.mkdir(path) + scene.save_image(path, name) diff --git a/scripts/ecf_graph_scenes.py b/scripts/ecf_graph_scenes.py index 78e71235..5305be47 100644 --- a/scripts/ecf_graph_scenes.py +++ b/scripts/ecf_graph_scenes.py @@ -10,11 +10,16 @@ from animation import * from mobject import * from constants import * from region import * +import displayer as disp from scene import Scene, GraphScene +from scene.graphs import * from moser_main import EulersFormula from script_wrapper import command_line_create_scene MOVIE_PREFIX = "ecf_graph_scenes/" +RANDOLPH_SCALE_VAL = 0.3 +EDGE_ANNOTATION_SCALE_VAL = 0.7 +DUAL_CYCLE = [3, 4, 5, 6, 1, 0, 2, 3] class IntroduceGraphs(GraphScene): def construct(self): @@ -25,7 +30,7 @@ class IntroduceGraphs(GraphScene): self.clear() self.add(*self.edges) self.replace_vertices_with(SimpleFace().scale(0.4)) - friends = text_mobject("Friends").scale(0.5) + friends = text_mobject("Friends").scale(EDGE_ANNOTATION_SCALE_VAL) self.annotate_edges(friends.shift((0, friends.get_height()/2, 0))) self.animate(*[ SemiCircleTransform(vertex, Dot(point)) @@ -41,8 +46,874 @@ class IntroduceGraphs(GraphScene): class PlanarGraphDefinition(Scene): def construct(self): + Not, quote, planar, end_quote = text_mobject([ + "Not \\\\", "``", "Planar", "''", + # "no matter how \\\\ hard you try" + ]) + shift_val = CompoundMobject(Not, planar).to_corner().get_center() + Not.highlight("red").shift(shift_val) + graphs = [ + CompoundMobject(*GraphScene(g).mobjects) + for g in [ + CUBE_GRAPH, + complete_graph(5), + OCTOHEDRON_GRAPH + ] + ] + + self.add(quote, planar, end_quote) + self.dither() + self.animate( + FadeOut(quote), + FadeOut(end_quote), + ApplyMethod(planar.shift, shift_val), + FadeIn(graphs[0]), + run_time = 1.5 + ) + self.dither() + self.remove(graphs[0]) + self.add(graphs[1]) + planar.highlight("red") + self.add(Not) + self.dither(2) + planar.highlight("white") + self.remove(Not) + self.remove(graphs[1]) + self.add(graphs[2]) + self.dither(2) + + +class TerminologyFromPolyhedra(GraphScene): + args_list = [(CUBE_GRAPH,)] + def construct(self): + GraphScene.construct(self) + rot_kwargs = { + "radians" : np.pi / 3, + "run_time" : 5.0 + } + vertices = [ + point / 2 + OUT if abs(point[0]) == 2 else point + IN + for point in self.points + ] + cube = CompoundMobject(*[ + Line(vertices[edge[0]], vertices[edge[1]]) + for edge in self.graph["edges"] + ]) + cube.rotate(-np.pi/3, [0, 0, 1]) + cube.rotate(-np.pi/3, [0, 1, 0]) + dots_to_vertices = text_mobject("Dots $\\to$ Vertices").to_corner() + lines_to_edges = text_mobject("Lines $\\to$ Edges").to_corner() + regions_to_faces = text_mobject("Regions $\\to$ Faces").to_corner() + self.clear() + self.animate(TransformAnimations( + Rotating(Dodecahedron(), **rot_kwargs), + Rotating(cube, **rot_kwargs), + )) + self.clear() + self.animate(*[ + Transform(l1, l2) + for l1, l2 in zip(cube.split(), self.edges) + ]) + self.dither() + self.add(dots_to_vertices) + self.animate(*[ + ShowCreation(dot, run_time = 1.0) + for dot in self.vertices + ]) + self.dither(2) + self.remove(dots_to_vertices, *self.vertices) + self.add(lines_to_edges) + self.animate(ApplyMethod( + CompoundMobject(*self.edges).highlight, "yellow" + )) + self.dither(2) + self.clear() + self.add(*self.edges) + self.add(regions_to_faces) + self.generate_regions() + for region in self.regions: + self.highlight_region(region) + self.dither(3.0) + + +class ThreePiecesOfTerminology(GraphScene): + def construct(self): + GraphScene.construct(self) + terms = cycles, spanning_trees, dual_graphs = [ + text_mobject(phrase).shift(y*UP).to_edge() + for phrase, y in [ + ("Cycles", 3), + ("Spanning Trees", 1), + ("Dual Graphs", -1), + ] + ] + self.generate_spanning_tree() + scale_val = 1.2 + def accent(mobject, color = "yellow"): + return mobject.scale_in_place(scale_val).highlight(color) + def tone_down(mobject): + return mobject.scale_in_place(1.0/scale_val).highlight("white") + + self.add(accent(cycles)) + self.trace_cycle(run_time = 1.0) + self.dither() + tone_down(cycles) + self.remove(self.traced_cycle) + + self.add(accent(spanning_trees)) + self.animate(ShowCreation(self.spanning_tree), run_time = 1.0) + self.dither() + tone_down(spanning_trees) + self.remove(self.spanning_tree) + + self.add(accent(dual_graphs, "red")) + self.generate_dual_graph() + for mob in self.mobjects: + mob.fade + self.animate(*[ + ShowCreation(mob, run_time = 1.0) + for mob in self.dual_vertices + self.dual_edges + ]) + self.dither() + + self.clear() + self.animate(ApplyMethod( + CompoundMobject(*terms).center + )) + self.dither() + +class WalkingRandolph(GraphScene): + args_list = [ + (SAMPLE_GRAPH, [0, 1, 7, 8]), + ] + @staticmethod + def args_to_string(graph, path): + return graph["name"] + "".join(map(str, path)) + + def __init__(self, graph, path, *args, **kwargs): + self.path = path + GraphScene.__init__(self, graph, *args, **kwargs) + + def construct(self): + GraphScene.construct(self) + point_path = [self.points[i] for i in self.path] + randy = Randolph() + randy.scale(RANDOLPH_SCALE_VAL) + randy.move_to(point_path[0]) + for next, last in zip(point_path[1:], point_path): + self.animate( + WalkPiCreature(randy, next), + ShowCreation(Line(last, next).highlight("yellow")), + run_time = 2.0 + ) + self.randy = randy + + +class PathExamples(GraphScene): + args_list = [(SAMPLE_GRAPH,)] + def construct(self): + GraphScene.construct(self) + paths = [ + (1, 2, 4, 5, 6), + (6, 7, 1, 3), + ] + non_paths = [ + [(0, 1), (7, 8), (5, 6),], + [(5, 0), (0, 2), (0, 1)], + ] + valid_path = text_mobject("Valid \\\\ Path").highlight("green") + not_a_path = text_mobject("Not a \\\\ Path").highlight("red") + for mob in valid_path, not_a_path: + mob.to_edge(UP) + kwargs = {"run_time" : 1.0} + for path, non_path in zip(paths, non_paths): + path_lines = CompoundMobject(*[ + Line( + self.points[path[i]], + self.points[path[i+1]] + ).highlight("yellow") + for i in range(len(path) - 1) + ]) + non_path_lines = CompoundMobject(*[ + Line( + self.points[pp[0]], + self.points[pp[1]], + ).highlight("yellow") + for pp in non_path + ]) + + self.remove(not_a_path) + self.add(valid_path) + self.animate(ShowCreation(path_lines, **kwargs)) + self.dither(2) + self.remove(path_lines) + + self.remove(valid_path) + self.add(not_a_path) + self.animate(ShowCreation(non_path_lines, **kwargs)) + self.dither(2) + self.remove(non_path_lines) + +class IntroduceCycle(WalkingRandolph): + args_list = [ + (SAMPLE_GRAPH, [0, 1, 3, 2, 0]) + ] + def construct(self): + WalkingRandolph.construct(self) + self.remove(self.randy) + encompassed_cycles = filter( + lambda c : set(c).issubset(self.path), + self.graph["region_cycles"] + ) + regions = [ + self.region_from_cycle(cycle) + for cycle in encompassed_cycles + ] + for region in regions: + self.highlight_region(region) + self.dither() + + + +class IntroduceRandolph(GraphScene): + def construct(self): + GraphScene.construct(self) + randy = Randolph().move_to((-3, 0, 0)) + name = text_mobject("Randolph") + self.animate(Transform( + randy, + deepcopy(randy).scale(RANDOLPH_SCALE_VAL).move_to(self.points[0]), + )) + self.dither() + name.shift((0, 1, 0)) + self.add(name) + self.dither() + +class DefineSpanningTree(GraphScene): + def construct(self): + GraphScene.construct(self) + randy = Randolph() + randy.scale(RANDOLPH_SCALE_VAL).move_to(self.points[0]) + dollar_signs = text_mobject("\\$\\$") + dollar_signs.scale(EDGE_ANNOTATION_SCALE_VAL) + dollar_signs = CompoundMobject(*[ + deepcopy(dollar_signs).shift(edge.get_center()) + for edge in self.edges + ]) + unneeded = text_mobject("unneeded!") + unneeded.scale(EDGE_ANNOTATION_SCALE_VAL) + self.generate_spanning_tree() + def green_dot_at_index(index): + return Dot( + self.points[index], + radius = 2*Dot.DEFAULT_RADIUS, + color = "lightgreen", + ) + def out_of_spanning_set(point_pair): + stip = self.spanning_tree_index_pairs + return point_pair not in stip and \ + tuple(reversed(point_pair)) not in stip + + self.add(randy) + self.accent_vertices(run_time = 2.0) + self.add(dollar_signs) + self.dither(2) + self.remove(dollar_signs) + run_time_per_branch = 0.5 + self.animate( + ShowCreation(green_dot_at_index(0)), + run_time = run_time_per_branch + ) + for pair in self.spanning_tree_index_pairs: + self.animate(ShowCreation( + Line( + self.points[pair[0]], + self.points[pair[1]] + ).highlight("yellow"), + run_time = run_time_per_branch + )) + self.animate(ShowCreation( + green_dot_at_index(pair[1]), + run_time = run_time_per_branch + )) + self.dither(2) + + unneeded_edges = filter(out_of_spanning_set, self.graph["edges"]) + for edge, limit in zip(unneeded_edges, range(5)): + line = Line(self.points[edge[0]], self.points[edge[1]]) + line.highlight("red") + self.animate(ShowCreation(line, run_time = 1.0)) + self.add(unneeded.center().shift(line.get_center() + 0.2*UP)) + self.dither() + self.remove(line, unneeded) + +class NamingTree(GraphScene): + def construct(self): + GraphScene.construct(self) + self.generate_spanning_tree() + self.generate_treeified_spanning_tree() + branches = self.spanning_tree.split() + branches_copy = deepcopy(branches) + treeified_branches = self.treeified_spanning_tree.split() + tree = text_mobject("``Tree''").to_edge(UP) + spanning_tree = text_mobject("``Spanning Tree''").to_edge(UP) + + self.add(*branches) + self.animate( + FadeOut(CompoundMobject(*self.edges + self.vertices)), + Animation(CompoundMobject(*branches)), + ) + self.clear() + self.add(tree, *branches) + self.dither() + self.animate(*[ + Transform(b1, b2, run_time = 2) + for b1, b2 in zip(branches, treeified_branches) + ]) + self.dither() + self.animate(*[ + FadeIn(mob) + for mob in self.edges + self.vertices + ] + [ + Transform(b1, b2, run_time = 2) + for b1, b2 in zip(branches, branches_copy) + ]) + self.accent_vertices(run_time = 2) + self.remove(tree) + self.add(spanning_tree) + self.dither(2) + +class DualGraph(GraphScene): + def construct(self): + GraphScene.construct(self) + self.generate_dual_graph() + self.add(text_mobject("Dual Graph").to_edge(UP).shift(2*LEFT)) + self.animate(*[ + ShowCreation(mob) + for mob in self.dual_edges + self.dual_vertices + ]) + self.dither() + +class FacebookLogo(Scene): + def construct(self): + im = ImageMobject("facebook_full_logo", invert = False) + self.add(im.scale(0.7)) + + +class FacebookGraph(GraphScene): + def construct(self): + GraphScene.construct(self) + account = ImageMobject("facebook_silhouette", invert = False) + account.scale(0.05) + logo = ImageMobject("facebook_logo", invert = False) + logo.scale(0.1) + logo.shift(0.2*LEFT + 0.1*UP) + account.add(logo).center() + account.shift(0.2*LEFT + 0.1*UP) + friends = tex_mobject( + "\\leftarrow \\text{friends} \\rightarrow" + ).scale(0.5*EDGE_ANNOTATION_SCALE_VAL) + + self.clear() + accounts = [ + deepcopy(account).shift(point) + for point in self.points + ] + self.add(*accounts) + self.dither() + self.annotate_edges(friends) + self.dither() + self.animate(*[ + SemiCircleTransform(account, vertex) + for account, vertex in zip(accounts, self.vertices) + ]) + self.dither() + self.animate(*[ + Transform(ann, edge) + for ann, edge in zip(self.edge_annotations, self.edges) + ]) + self.dither() + +class FacebookGraphAsAbstractSet(Scene): + def construct(self): + names = [ + "Louis", + "Randolph", + "Mortimer", + "Billy Ray", + "Penelope", + ] + friend_pairs = [ + (0, 1), + (0, 2), + (1, 2), + (3, 0), + (4, 0), + (1, 3), + (1, 2), + ] + names_mob = text_mobject("\\\\".join(names)).shift(3*LEFT) + friends_mob = tex_mobject("\\\\".join([ + "\\text{%s}&\\leftrightarrow\\text{%s}"%(names[i],names[j]) + for i, j in friend_pairs + ]), size = "\\Large").shift(3*RIGHT) + accounts = text_mobject("\\textbf{Accounts}") + accounts.shift(3*LEFT).to_edge(UP) + friendships = text_mobject("\\textbf{Friendships}") + friendships.shift(3*RIGHT).to_edge(UP) + lines = CompoundMobject( + Line(UP*SPACE_HEIGHT, DOWN*SPACE_HEIGHT), + Line(LEFT*SPACE_WIDTH + 3*UP, RIGHT*SPACE_WIDTH + 3*UP) + ).highlight("white") + + self.add(accounts, friendships, lines) + self.dither() + for mob in names_mob, friends_mob: + self.animate(ShowCreation( + mob, run_time = 1.0 + )) + self.dither() + + +class ExamplesOfGraphs(GraphScene): + def construct(self): + buff = 0.5 + self.graph["vertices"] = map( + lambda v : v + DOWN + RIGHT, + self.graph["vertices"] + ) + GraphScene.construct(self) + objects, notion = CompoundMobject(*text_mobject( + ["Objects \\quad\\quad ", "Thing that connects objects"] + )).to_corner().shift(0.5*RIGHT).split() + horizontal_line = Line( + (-SPACE_WIDTH, SPACE_HEIGHT-1, 0), + (max(notion.points[:,0]), SPACE_HEIGHT-1, 0) + ) + vert_line_x_val = min(notion.points[:,0]) - buff + vertical_line = Line( + (vert_line_x_val, SPACE_HEIGHT, 0), + (vert_line_x_val,-SPACE_HEIGHT, 0) + ) + objects_and_notion_strings = [ + ("Facebook accounts", "Friendship"), + ("Cities", "Roads between them"), + ("Wikipedia pages", "Links"), + ("Neurons", "Synapses"), + ( + "Regions our graph \\\\ cuts the plane into", + "Shareed edges" + ) + ] + objects_and_notions = [ + ( + text_mobject(obj, size = "\\small").to_edge(LEFT), + text_mobject(no, size = "\\small").to_edge(LEFT).shift( + (vert_line_x_val + SPACE_WIDTH)*RIGHT + ) + ) + for obj, no in objects_and_notion_strings + ] + + self.clear() + self.add(objects, notion, horizontal_line, vertical_line) + for (obj, notion), height in zip(objects_and_notions, it.count(2, -1)): + if obj == objects_and_notions[-1][0]: + obj.highlight("yellow") + notion.highlight("yellow") + self.animate(*[ + ShowCreation(mob, run_time = 1.0) + for mob in self.edges + self.vertices + ]) + self.dither() + self.generate_regions() + for region in self.regions: + self.highlight_region(region) + self.add(obj.shift(height*UP)) + self.dither(1) + self.add(notion.shift(height*UP)) + if obj == objects_and_notions[-1][0]: + self.animate(*[ + ShowCreation(deepcopy(e).highlight(), run_time = 1) + for e in self.edges + ]) + self.dither() + else: + self.dither(2) + +class DrawDualGraph(GraphScene): + def construct(self): + GraphScene.construct(self) + self.generate_regions() + self.generate_dual_graph() + region_mobs = [ + ImageMobject(disp.paint_region(reg, self.background), invert = False) + for reg in self.regions + ] + for region, mob in zip(self.regions, region_mobs): + self.highlight_region(region, mob.get_color()) + outer_region = self.regions.pop() + outer_region_mob = region_mobs.pop() + outer_dual_vertex = self.dual_vertices.pop() + internal_edges = filter( + lambda e : abs(e.start[0]) < SPACE_WIDTH and \ + abs(e.end[0]) < SPACE_WIDTH and \ + abs(e.start[1]) < SPACE_HEIGHT and \ + abs(e.end[1]) < SPACE_HEIGHT, + self.dual_edges + ) + external_edges = filter( + lambda e : e not in internal_edges, + self.dual_edges + ) + + self.dither() + self.reset_background() + self.highlight_region(outer_region, outer_region_mob.get_color()) + self.animate(*[ + Transform(reg_mob, dot) + for reg_mob, dot in zip(region_mobs, self.dual_vertices) + ]) + self.dither() + self.reset_background() + self.animate(ApplyFunction( + lambda p : (SPACE_WIDTH + SPACE_HEIGHT)*p/np.linalg.norm(p), + outer_region_mob + )) + self.dither() + for edges in internal_edges, external_edges: + self.animate(*[ + ShowCreation(edge, run_time = 2.0) + for edge in edges + ]) + self.dither() + +class EdgesAreTheSame(GraphScene): + def construct(self): + GraphScene.construct(self) + self.generate_dual_graph() + self.remove(*self.vertices) + self.add(*self.dual_edges) + self.dither() + self.animate(*[ + Transform(*pair, run_time = 2.0) + for pair in zip(self.dual_edges, self.edges) + ]) + self.dither() + self.add( + text_mobject(""" + (Or at least I would argue they should \\\\ + be thought of as the same thing.) + """, size = "\\small").to_edge(UP) + ) + self.dither() + +class ListOfCorrespondances(Scene): + def construct(self): + buff = 0.5 + correspondances = [ + ["Regions cut out by", "Vertices of"], + ["Edges of", "Edges of"], + ["Cycles of", "Connected components of"], + ["Connected components of", "Cycles of"], + ["Spanning tree in", "Complement of spanning tree in"], + ["", "Dual of"], + ] + for corr in correspondances: + corr[0] += " original graph" + corr[1] += " dual graph" + arrow = tex_mobject("\\leftrightarrow", size = "\\large") + lines = [] + for corr, height in zip(correspondances, it.count(3, -1)): + left = text_mobject(corr[0], size = "\\small") + right = text_mobject(corr[1], size = "\\small") + this_arrow = deepcopy(arrow) + for mob in left, right, this_arrow: + mob.shift(height*UP) + arrow_xs = this_arrow.points[:,0] + left.to_edge(RIGHT) + left.shift((min(arrow_xs) - SPACE_WIDTH, 0, 0)) + right.to_edge(LEFT) + right.shift((max(arrow_xs) + SPACE_WIDTH, 0, 0)) + lines.append(CompoundMobject(left, right, this_arrow)) + last = None + for line in lines: + self.add(line.highlight("yellow")) + if last: + last.highlight("white") + last = line + self.dither(1) + + +class CyclesCorrespondWithConnectedComponents(GraphScene): + args_list = [(SAMPLE_GRAPH,)] + def construct(self): + GraphScene.construct(self) + self.generate_regions() + self.generate_dual_graph() + cycle = [4, 2, 1, 5, 4] + enclosed_regions = [0, 2, 3, 4] + dual_cycle = DUAL_CYCLE + enclosed_vertices = [0, 1] + randy = Randolph() + randy.scale(RANDOLPH_SCALE_VAL) + randy.move_to(self.points[cycle[0]]) + + lines_to_remove = [] + for last, next in zip(cycle, cycle[1:]): + line = Line(self.points[last], self.points[next]) + line.highlight("yellow") + self.animate( + ShowCreation(line), + WalkPiCreature(randy, self.points[next]), + run_time = 1.0 + ) + lines_to_remove.append(line) + self.dither() + self.remove(randy, *lines_to_remove) + for region in np.array(self.regions)[enclosed_regions]: + self.highlight_region(region) + self.dither(2) + self.reset_background() + lines = CompoundMobject(*[ + Line(self.dual_points[last], self.dual_points[next]) + for last, next in zip(dual_cycle, dual_cycle[1:]) + ]).highlight("red") + self.animate(ShowCreation(lines)) + self.animate(*[ + Transform(v, Dot( + v.get_center(), + radius = 3*Dot.DEFAULT_RADIUS + ).highlight("green")) + for v in np.array(self.vertices)[enclosed_vertices] + ] + [ + ApplyMethod(self.edges[0].highlight, "green") + ]) + self.dither() + + +class IntroduceMortimer(GraphScene): + args_list = [(SAMPLE_GRAPH,)] + def construct(self): + GraphScene.construct(self) + self.generate_dual_graph() + self.generate_regions() + randy = Randolph().shift(LEFT) + morty = Mortimer().shift(RIGHT) + name = text_mobject("Mortimer") + name.shift(morty.get_center() + 1.2*UP) + randy_path = (0, 1, 3) + morty_path = (-2, -3, -4) + morty_crossed_lines = [ + Line(self.points[i], self.points[j]).highlight("red") + for i, j in [(7, 1), (1, 5)] + ] + kwargs = {"run_time" : 1.0} + + self.clear() + self.add(randy) + self.dither() + self.add(morty, name) + self.dither() + self.remove(name) + small_randy = deepcopy(randy).scale(RANDOLPH_SCALE_VAL) + small_morty = deepcopy(morty).scale(RANDOLPH_SCALE_VAL) + small_randy.move_to(self.points[randy_path[0]]) + small_morty.move_to(self.dual_points[morty_path[0]]) + self.animate(*[ + FadeIn(mob) + for mob in self.vertices + self.edges + ] + [ + Transform(randy, small_randy), + Transform(morty, small_morty), + ], **kwargs) + self.dither() + + + self.highlight_region(self.regions[morty_path[0]]) + for last, next in zip(morty_path, morty_path[1:]): + self.animate(WalkPiCreature(morty, self.dual_points[next]),**kwargs) + self.highlight_region(self.regions[next]) + self.dither() + for last, next in zip(randy_path, randy_path[1:]): + line = Line(self.points[last], self.points[next]) + line.highlight("yellow") + self.animate( + WalkPiCreature(randy, self.points[next]), + ShowCreation(line), + **kwargs + ) + self.dither() + self.animate(*[ + ApplyMethod( + line.rotate_in_place, + np.pi/10, + alpha_func = wiggle) + for line in morty_crossed_lines + ], **kwargs) + + self.dither() + +class RandolphMortimerSpanningTreeGame(GraphScene): + args_list = [(SAMPLE_GRAPH,)] + def construct(self): + GraphScene.construct(self) + self.generate_spanning_tree() + self.generate_dual_graph() + self.generate_regions() + randy = Randolph().scale(RANDOLPH_SCALE_VAL) + morty = Mortimer().scale(RANDOLPH_SCALE_VAL) + randy.move_to(self.points[0]) + morty.move_to(self.dual_points[0]) + attempted_dual_point_index = 2 + region_ordering = [0, 1, 7, 2, 3, 5, 4, 6] + dual_edges = [1, 3, 4, 7, 11, 9, 13] + time_per_dual_edge = 0.5 + + self.add(randy, morty) + self.animate(ShowCreation(self.spanning_tree)) + self.dither() + self.animate(WalkPiCreature( + morty, self.dual_points[attempted_dual_point_index], + alpha_func = lambda t : 0.3 * there_and_back(t), + run_time = 2.0, + )) + self.dither() + for index in range(len(self.regions)): + if index > 0: + edge = self.edges[dual_edges[index-1]] + midpoint = edge.get_center() + self.animate(*[ + ShowCreation(Line( + midpoint, + tip + ).highlight("red")) + for tip in edge.start, edge.end + ], run_time = time_per_dual_edge) + self.highlight_region(self.regions[region_ordering[index]]) + self.dither(time_per_dual_edge) + self.dither() + + + cycle_index = region_ordering[-1] + cycle = self.graph["region_cycles"][cycle_index] + self.highlight_region(self.regions[cycle_index], "black") + self.animate(ShowCreation(CompoundMobject(*[ + Line(self.points[last], self.points[next]).highlight("green") + for last, next in zip(cycle, list(cycle)[1:] + [cycle[0]]) + ]))) + self.dither() + +class MortimerCannotTraverseCycle(GraphScene): + args_list = [(SAMPLE_GRAPH,)] + def construct(self): + GraphScene.construct(self) + self.generate_dual_graph() + dual_cycle = DUAL_CYCLE + trapped_points = [0, 1] + morty = Mortimer().scale(RANDOLPH_SCALE_VAL) + morty.move_to(self.dual_points[dual_cycle[0]]) + time_per_edge = 0.5 + text = text_mobject(""" + One of these lines must be included + in the spanning tree if those two inner + vertices are to be reached. + """).scale(0.7).to_edge(UP) + + all_lines = [] + matching_edges = [] + kwargs = {"run_time" : time_per_edge, "alpha_func" : None} + for last, next in zip(dual_cycle, dual_cycle[1:]): + line = Line(self.dual_points[last], self.dual_points[next]) + line.highlight("red") + self.animate( + WalkPiCreature(morty, self.dual_points[next], **kwargs), + ShowCreation(line, **kwargs), + ) + all_lines.append(line) + center = line.get_center() + distances = map( + lambda e : np.linalg.norm(center - e.get_center()), + self.edges + ) + matching_edges.append( + self.edges[distances.index(min(distances))] + ) + self.animate(*[ + Transform(v, Dot( + v.get_center(), + radius = 3*Dot.DEFAULT_RADIUS, + color = "green" + )) + for v in np.array(self.vertices)[trapped_points] + ]) + self.add(text) + self.animate(*[ + Transform(line, deepcopy(edge).highlight(line.get_color())) + for line, edge in zip(all_lines, matching_edges) + ]) + self.dither() + +class TreeCountFormula(Scene): + def construct(self): + time_per_branch = 0.5 + self.add(text_mobject(""" + In any tree: + $$E + 1 = V$$ + """)) + gs = GraphScene(SAMPLE_GRAPH) + gs.generate_treeified_spanning_tree() + branches = gs.treeified_spanning_tree.to_edge(LEFT).split() + self.add(Dot(branches[0].points[0])) + for branch in branches: + self.animate( + ShowCreation(branch), + run_time = time_per_branch + ) + self.add(Dot(branch.points[-1])) + self.dither() + +class FinalSum(Scene): + def construct(self): + lines = tex_mobject([ + "(\\text{Number of Randolph's Edges}) + 1 &= V \\\\ \n", + "(\\text{Number of Mortimer's Edges}) + 1 &= F \\\\ \n", + " \\Downarrow \\\\", "E","+","2","&=","V","+","F", + ], size = "\\large") + for line in lines[:2] + [CompoundMobject(*lines[2:])]: + self.add(line) + self.dither() + self.dither() + + symbols = V, minus, E, plus, F, equals, two = tex_mobject( + "V - E + F = 2".split(" ") + ) + plus = tex_mobject("+") + anims = [] + for mob, index in zip(symbols, [-3, -2, -7, -6, -1, -4, -5]): + copy = plus if index == -2 else deepcopy(mob) + copy.center().shift(lines[index].get_center()) + copy.scale_in_place(lines[index].get_width()/mob.get_width()) + anims.append(SemiCircleTransform(copy, mob)) + self.clear() + self.animate(*anims, run_time = 2.0) + self.dither() + + + if __name__ == "__main__": - command_line_create_scene(MOVIE_PREFIX) \ No newline at end of file + command_line_create_scene(MOVIE_PREFIX) + + + + + + + + + + +