Nearly done with ecf

This commit is contained in:
Grant Sanderson
2015-06-19 08:31:02 -07:00
parent 344290068c
commit fc395d14e1
18 changed files with 1487 additions and 185 deletions

View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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")

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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:])
command_line_create_scene()

View File

@ -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()
)

88
scene/pascals_triangle.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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
@ -167,3 +86,4 @@ class PascalsTriangleScene(Scene):
mob.shift(self.coords_to_center(n, k))
self.coords_to_mobs[n][k] = mob
self.add(mob)

View File

@ -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)

View File

@ -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)