Starting complex number animations

This commit is contained in:
Grant Sanderson 2015-10-12 19:39:46 -07:00
parent 7564beb393
commit c3cdafcfeb
11 changed files with 314 additions and 63 deletions

View file

@ -30,6 +30,15 @@ def clockwise_path(start_points, end_points, alpha):
def counterclockwise_path(start_points, end_points, alpha):
return semi_circular_path(start_points, end_points, alpha, OUT)
def get_best_interpolation_function(angle):
angle = (angle + np.pi)%(2*np.pi) - np.pi
if abs(angle) < np.pi/2:
return straight_path
elif angle > 0:
return counterclockwise_path
else:
return clockwise_path
class Transform(Animation):
DEFAULT_CONFIG = {
"run_time" : DEFAULT_TRANSFORM_RUN_TIME,

View file

@ -60,7 +60,7 @@ TEX_IMAGE_DIR = os.path.join(IMAGE_DIR, "Tex")
MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects")
IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image")
for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TEX_DIR, TMP_IMAGE_DIR,
for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TEX_DIR,
TEX_IMAGE_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR]:
if not os.path.exists(folder):
os.mkdir(folder)

View file

@ -49,49 +49,58 @@ def paint_mobjects(mobjects, image_array = None):
for mobject in mobjects:
if mobject.get_num_points() == 0:
continue
points, rgbs = place_on_screen(mobject.points, mobject.rgbs,
space_width, space_height)
points_and_rgbs = np.append(
mobject.points,
255*mobject.rgbs,
axis = 1
)
points_and_rgbs = place_on_screen(
points_and_rgbs,
space_width, space_height
)
#Map points to pixel space, which requires rescaling and shifting
#Remember, 2*space_height -> height
points[:,0] = points[:,0]*width/space_width/2 + width/2
points_and_rgbs[:,0] = points_and_rgbs[:,0]*width/space_width/2 + width/2
#Flip on y-axis as you go
points[:,1] = -1*points[:,1]*height/space_height/2 + height/2
points, rgbs = add_thickness(
points.astype('int'), rgbs,
points_and_rgbs[:,1] = -1*points_and_rgbs[:,1]*height/space_height/2 + height/2
points_and_rgbs = add_thickness(
points_and_rgbs.astype('int'),
mobject.point_thickness,
width, height
)
points, rgbs = points_and_rgbs[:,:2], points_and_rgbs[:,2:]
flattener = np.array([[1], [width]], dtype = 'int')
indices = np.dot(points, flattener)[:,0]
pixels[indices] = (255*rgbs).astype('uint8')
pixels[indices] = rgbs.astype('uint8')
pixels = pixels.reshape((height, width, 3)).astype('uint8')
return pixels
def add_thickness(pixel_indices, rgbs, thickness, width, height):
def add_thickness(pixel_indices_and_rgbs, thickness, width, height):
"""
Imagine dragging each pixel around like a paintbrush in
a plus-sign-shaped pixel arrangement surrounding it
a plus-sign-shaped pixel arrangement surrounding it.
Pass rgb = None to do nothing to them
"""
thickness = adjusted_thickness(thickness, width, height)
original = np.array(pixel_indices)
original_rgbs = np.array(rgbs)
original = np.array(pixel_indices_and_rgbs)
n_extra_columns = pixel_indices_and_rgbs.shape[1] - 2
for nudge in range(-thickness/2+1, thickness/2+1):
if nudge == 0:
continue
for x, y in [[nudge, 0], [0, nudge]]:
pixel_indices = np.append(
pixel_indices,
original+[x, y],
pixel_indices_and_rgbs = np.append(
pixel_indices_and_rgbs,
original+([x, y] + [0]*n_extra_columns),
axis = 0
)
rgbs = np.append(rgbs, original_rgbs, axis = 0)
admissibles = (pixel_indices[:,0] >= 0) & \
(pixel_indices[:,0] < width) & \
(pixel_indices[:,1] >= 0) & \
(pixel_indices[:,1] < height)
return pixel_indices[admissibles], rgbs[admissibles]
admissibles = (pixel_indices_and_rgbs[:,0] >= 0) & \
(pixel_indices_and_rgbs[:,0] < width) & \
(pixel_indices_and_rgbs[:,1] >= 0) & \
(pixel_indices_and_rgbs[:,1] < height)
return pixel_indices_and_rgbs[admissibles]
def adjusted_thickness(thickness, width, height):
big_width = PRODUCTION_QUALITY_DISPLAY_CONFIG["width"]
@ -99,38 +108,50 @@ def adjusted_thickness(thickness, width, height):
factor = (big_width + big_height) / (width + height)
return 1 + (thickness-1)/factor
def place_on_screen(points, rgbs, space_width, space_height):
def place_on_screen(points_and_rgbs, space_width, space_height):
"""
Projects points to 2d space and remove those outside a
the space constraints
the space constraints.
Pass rbgs = None to do nothing to them.
"""
# camera_distance = 10
points = np.array(points[:, :2])
# for i in range(2):
# points[:,i] *= camera_distance/(camera_distance-mobject.points[:,2])
rgbs = np.array(rgbs)
# Remove 3rd column
points_and_rgbs = np.append(
points_and_rgbs[:, :2],
points_and_rgbs[:, 3:],
axis = 1
)
#Removes points out of space
to_keep = (abs(points[:,0]) < space_width) & \
(abs(points[:,1]) < space_height)
return points[to_keep], rgbs[to_keep]
to_keep = (abs(points_and_rgbs[:,0]) < space_width) & \
(abs(points_and_rgbs[:,1]) < space_height)
return points_and_rgbs[to_keep]
def get_file_path(name, extension):
file_path = os.path.join(MOVIE_DIR, name)
if not file_path.endswith(".mp4"):
file_path += ".mp4"
directory = os.path.split(file_path)[0]
if not os.path.exists(directory):
os.makedirs(directory)
return file_path
def write_to_gif(scene, name):
#TODO, find better means of compression
if not name.endswith(".gif"):
name += ".gif"
filepath = os.path.join(GIF_DIR, name)
file_path = os.path.join(GIF_DIR, name)
temppath = os.path.join(GIF_DIR, "Temp.gif")
print "Writing " + name + "..."
images = [Image.fromarray(frame) for frame in scene.frames]
writeGif(temppath, images, scene.frame_duration)
print "Compressing..."
os.system("gifsicle -O " + temppath + " > " + filepath)
os.system("gifsicle -O " + temppath + " > " + file_path)
os.system("rm " + temppath)
def write_to_movie(scene, name):
filepath = os.path.join(MOVIE_DIR, name) + ".mp4"
print "Writing to %s"%filepath
file_path = get_file_path(name, ".mp4")
print "Writing to %s"%file_path
fps = int(1/scene.display_config["frame_duration"])
dim = (scene.display_config["width"], scene.display_config["height"])
@ -149,12 +170,11 @@ def write_to_movie(scene, name):
'-c:v', 'libx264',
'-pix_fmt', 'yuv420p',
'-loglevel', 'error',
filepath,
file_path,
]
process = sp.Popen(command, stdin=sp.PIPE)
for frame in scene.frames:
process.stdin.write(frame.tostring())
process.stdin.close()
process.wait()

View file

@ -2,6 +2,7 @@ import numpy as np
import itertools as it
from mobject import Mobject, Mobject1D, Mobject2D, CompoundMobject
from simple_mobjects import Arrow, Line
from image_mobject import tex_mobject
from constants import *
from helpers import *
@ -255,9 +256,65 @@ class NumberPlane(Mobject1D):
self.add(*self.get_coordinate_labels(x_vals, y_vals))
return self
def get_vector(self, coords, **kwargs):
if len(coords) == 2:
coords = tuple(list(coords) + [0])
arrow = Arrow(ORIGIN, coords, **kwargs)
arrow.remove_tip()
arrow.align_data(Line(ORIGIN, SPACE_WIDTH*LEFT))
arrow.add_tip()
return arrow
class ComplexPlane(NumberPlane):
#TODO
pass
DEFAULT_CONFIG = {
"color" : "lightgreen",
"unit_to_spatial_width" : 1,
"line_frequency" : 1,
"faded_line_frequency" : 0.5,
"number_at_center" : complex(0),
}
def __init__(self, **kwargs):
digest_config(self, ComplexPlane, kwargs)
kwargs.update({
"x_unit_to_spatial_width" : self.unit_to_spatial_width,
"y_uint_to_spatial_height" : self.unit_to_spatial_width,
"x_line_frequency" : self.line_frequency,
"x_faded_line_frequency" : self.faded_line_frequency,
"y_line_frequency" : self.line_frequency,
"y_faded_line_frequency" : self.faded_line_frequency,
"num_pair_at_center" : (self.number_at_center.real, self.number_at_center.imag),
})
NumberPlane.__init__(self, **kwargs)
def number_to_point(self, number):
number = complex(number)
return self.num_pair_to_point((number.real, number.imag))
def get_coordinate_labels(self, *numbers):
result = []
nudge = 0.1*(DOWN+RIGHT)
if len(numbers) == 0:
numbers = range(-int(self.x_radius), int(self.x_radius))
numbers += [
complex(0, y)
for y in range(-int(self.y_radius), int(self.y_radius))
]
for number in numbers:
point = self.number_to_point(number)
if number == 0:
num_str = "0"
else:
num_str = str(number).replace("j", "i")
num = tex_mobject(num_str)
num.scale(self.number_scale_factor)
num.shift(point-num.get_corner(UP+LEFT)+nudge)
result.append(num)
return result
def add_coordinates(self, *numbers):
self.add(*self.get_coordinate_labels(*numbers))
return self

View file

@ -321,18 +321,17 @@ class Mobject(object):
#Typically implemented in subclass, unless purposefully left blank
pass
### Static Methods ###
def align_data(mobject1, mobject2):
count1, count2 = mobject1.get_num_points(), mobject2.get_num_points()
def align_data(self, mobject):
count1, count2 = self.get_num_points(), mobject.get_num_points()
if count1 == 0:
mobject1.add_points([(0, 0, 0)])
self.add_points([(0, 0, 0)])
if count2 == 0:
mobject2.add_points([(0, 0, 0)])
mobject.add_points([(0, 0, 0)])
if count1 == count2:
return
for attr in ['points', 'rgbs']:
new_arrays = make_even(getattr(mobject1, attr), getattr(mobject2, attr))
for array, mobject in zip(new_arrays, [mobject1, mobject2]):
new_arrays = make_even(getattr(self, attr), getattr(mobject, attr))
for array, mobject in zip(new_arrays, [self, mobject]):
setattr(mobject, attr, np.array(array))
def interpolate(mobject1, mobject2, target_mobject, alpha):
@ -342,9 +341,10 @@ class Mobject(object):
"""
Mobject.align_data(mobject1, mobject2)
for attr in ['points', 'rgbs']:
new_array = (1 - alpha) * getattr(mobject1, attr) + \
alpha * getattr(mobject2, attr)
setattr(target_mobject, attr, new_array)
setattr(target_mobject, attr, interpolate(
getattr(mobject1, attr),
getattr(mobject2, attr),
alpha))
#TODO, Make the two implementations bellow not redundant
class Mobject1D(Mobject):

View file

@ -16,9 +16,10 @@ from script_wrapper import command_line_create_scene
class SampleScene(Scene):
def construct(self):
circle = Circle().repeat(6)
self.play(Transform(circle, Square(), run_time = 3))
self.dither()
c = ComplexPlane().add_coordinates()
self.add_local_mobjects()
if __name__ == "__main__":

View file

@ -51,7 +51,7 @@ class Scene(object):
return self.name
return self.__class__.__name__ + \
self.args_to_string(*self.construct_args)
def set_name(self, name):
self.name = name
return self
@ -62,6 +62,8 @@ class Scene(object):
in the order with which they are entered.
"""
for mobject in mobjects:
if not isinstance(mobject, Mobject):
raise Exception("Adding something which is not a mobject")
#In case it's already in there, it should
#now be closer to the foreground.
self.remove(mobject)
@ -86,6 +88,15 @@ class Scene(object):
self.mobjects.remove(mobject)
return self
def bring_to_front(self, mobject):
self.add(mobject)
return self
def bring_to_back(self, mobject):
self.remove(mobject)
self.mobjects = [mobject] + self.mobjects
return self
def clear(self):
self.reset_background()
self.remove(*self.mobjects)
@ -174,11 +185,8 @@ class Scene(object):
animation.clean_up()
return self
def apply(self, mob_to_anim_func, **kwargs):
self.play(*[
mob_to_anim_func(mobject)
for mobject in self.mobjects
])
def apply(self, mobject_method, *args, **kwargs):
self.play(ApplyMethod(mobject_method, *args, **kwargs))
def get_frame(self):
return disp.paint_mobjects(self.mobjects, self.background)

View file

@ -7,7 +7,7 @@ import time
class TkSceneRoot(Tkinter.Tk):
def __init__(self, scene):
if scene.frames == []:
raise str(scene) + " has no frames!"
raise Exception(str(scene) + " has no frames!")
Tkinter.Tk.__init__(self)
height, width = scene.shape

View file

@ -66,7 +66,7 @@ def get_configuration(sys_argv):
if len(args) > 0:
config["scene_name"] = args[0]
if len(args) > 1:
config["args_extension"] = args[1]
config["args_extension"] = " ".join(args[1:])
return config
def handle_scene(scene, **config):
@ -128,13 +128,13 @@ def command_line_create_scene(movie_prefix = ""):
"announce_construction" : True
}
for SceneClass in scene_classes:
args_list = SceneClass.args_list or [()]
args_list = SceneClass.args_list
preset_extensions = [
SceneClass.args_to_string(*args)
for args in args_list
]
if config["write_all"]:
args_to_run = args_list
args_to_run = args_list or [()]
elif config["args_extension"] in preset_extensions:
index = preset_extensions.index(config["args_extension"])
args_to_run = [args_list[index]]

157
scripts/complex_actions.py Normal file
View file

@ -0,0 +1,157 @@
#!/usr/bin/env python
import numpy as np
import itertools as it
from copy import deepcopy
import sys
from animation import *
from mobject import *
from constants import *
from region import *
from scene import Scene
from script_wrapper import command_line_create_scene
MOVIE_PREFIX = "complex_actions/"
class ComplexMultiplication(Scene):
@staticmethod
def args_to_string(multiplier):
return filter(lambda c : c not in "()", str(multiplier))
@staticmethod
def string_to_args(num_string):
return complex(num_string)
def construct(self, multiplier):
norm = np.linalg.norm(multiplier)
arg = np.log(multiplier).imag
plane_config = {
"faded_line_frequency" : 0
}
if norm > 1:
plane_config["density"] = norm*DEFAULT_POINT_DENSITY_1D
radius = SPACE_WIDTH
if norm > 0 and norm < 1:
radius /= norm
plane_config["x_radius"] = plane_config["y_radius"] = radius
plane = ComplexPlane(**plane_config)
self.anim_config = {
"run_time" : 2.0,
"interpolation_function" : get_best_interpolation_function(arg)
}
background = ComplexPlane(color = "grey")
labels = background.get_coordinate_labels()
self.paint_into_background(background, *labels)
arrow, new_arrow = [
plane.get_vector(plane.number_to_point(z), color = "skyblue")
for z in [1, multiplier]
]
self.add(arrow)
self.additional_animations = [Transform(
arrow, new_arrow, **self.anim_config
)]
self.mobjects_to_multiply = [plane]
self.mobjects_to_move_without_molding = []
self.multiplier = multiplier
self.plane = plane
if self.__class__ == ComplexMultiplication:
self.apply_multiplication()
def apply_multiplication(self):
def func((x, y, z)):
complex_num = self.multiplier*complex(x, y)
return (complex_num.real, complex_num.imag, z)
mobjects = self.mobjects_to_multiply+self.mobjects_to_move_without_molding
mobjects += [anim.mobject for anim in self.additional_animations]
self.add(*mobjects)
full_multiplications = [
ApplyMethod(mobject.apply_function, func, **self.anim_config)
for mobject in self.mobjects_to_multiply
]
movements_with_plane = [
ApplyMethod(
mobject.shift,
func(mobject.get_center())-mobject.get_center(),
**self.anim_config
)
for mobject in self.mobjects_to_move_without_molding
]
self.dither()
self.play(*reduce(op.add, [
full_multiplications,
movements_with_plane,
self.additional_animations
]))
self.dither()
class MultiplicationWithDot(ComplexMultiplication):
@staticmethod
def args_to_string(multiplier, dot_coords):
start = ComplexMultiplication.args_to_string(multiplier)
return start + "WithDotAt%d-%d"%dot_coords[:2]
@staticmethod
def string_to_args(arg_string):
parts = arg_string.split()
if len(parts) < 2 or len(parts) > 3:
raise Exception("Invalid arguments")
multiplier = complex(parts[0])
tup_string = filter(lambda c : c not in "()", parts[1])
nums = tuple(map(int, tup_string.split(",")))[:2]
return multiplier, nums
def construct(self, multiplier, dot_coords):
ComplexMultiplication.construct(self, multiplier)
self.mobjects_to_move_without_molding.append(
Dot().shift(dot_coords)
)
self.apply_multiplication()
class ShowComplexPower(ComplexMultiplication):
@staticmethod
def args_to_string(multiplier, num_repeats):
start = ComplexMultiplication.args_to_string(multiplier)
return start + "ToThe%d"%num_repeats
@staticmethod
def string_to_args(arg_string):
parts = arg_string.split()
if len(parts) < 2 or len(parts) > 3:
raise Exception("Invalid arguments")
multiplier = complex(parts[0])
num_repeats = int(parts[1])
return multiplier, num_repeats
def construct(self, multiplier, num_repeats):
ComplexMultiplication.construct(self, multiplier)
for x in range(num_repeats):
arrow_transform = Transform(*[
self.plane.get_vector(point, color = "skyblue")
for z in [multiplier**(x), multiplier**(x+1)]
for point in [self.plane.number_to_point(z)]
], **self.anim_config)
self.remove(*[anim.mobject for anim in self.additional_animations])
self.additional_animations = [arrow_transform]
self.apply_multiplication()
if __name__ == "__main__":
command_line_create_scene(MOVIE_PREFIX)

View file

@ -224,7 +224,6 @@ class ShowMatrixTransform(TransformScene2D):
self.add_x_y_arrows()
else:
self.add_number_plane(**number_plane_config)
self.save_image()
if show_matrix:
self.add(matrix_mobject(matrix).to_corner(UP+LEFT))
def func(mobject):