mirror of
https://github.com/3b1b/manim.git
synced 2025-07-30 13:34:19 +08:00
Nearly done with ecf
This commit is contained in:
@ -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)
|
||||
|
||||
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
17
constants.py
17
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")
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
24
region.py
24
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)
|
||||
|
||||
|
||||
|
||||
|
@ -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()
|
283
scene/graphs.py
283
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()
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
88
scene/pascals_triangle.py
Normal file
88
scene/pascals_triangle.py
Normal 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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user