tex mobjects implemented

This commit is contained in:
Grant Sanderson
2015-03-26 22:49:22 -06:00
parent 5750a40bc6
commit 3ae36a3ac9
7 changed files with 291 additions and 92 deletions

20
Tex/template.tex Normal file
View File

@ -0,0 +1,20 @@
\documentclass[12pt]{beamer}
\usepackage[english]{babel}
\usepackage{mathtools}
\mode<presentation>
{
\usetheme{default} % or try Darmstadt, Madrid, Warsaw, ...
\usecolortheme{default} % or try albatross, beaver, crane, ...
\usefonttheme[onlymath]{serif} % or try serif, structurebold, ...
\setbeamertemplate{navigation symbols}{}
\setbeamertemplate{caption}[numbered]
}
\begin{document}
\begin{frame}
\centering
SizeHere
$YourTextHere$
\end{frame}
\end{document}

View File

@ -16,19 +16,19 @@ import displayer as disp
class Animation(object): class Animation(object):
def __init__(self, def __init__(self,
mobject, mobject_or_animation,
run_time = DEFAULT_ANIMATION_RUN_TIME, run_time = DEFAULT_ANIMATION_RUN_TIME,
alpha_func = high_inflection_0_to_1, alpha_func = high_inflection_0_to_1,
name = None): name = None):
if isinstance(mobject, type) and issubclass(mobject, Mobject): self.embedded_animation = None
self.mobject = mobject() if isinstance(mobject_or_animation, type) and issubclass(mobject_or_animation, Mobject):
self.starting_mobject = mobject() self.mobject = mobject_or_animation()
elif isinstance(mobject, Mobject): elif isinstance(mobject_or_animation, Mobject):
self.mobject = mobject self.mobject = mobject_or_animation
self.starting_mobject = copy.deepcopy(mobject)
else: else:
raise Exception("Invalid mobject parameter, must be \ raise Exception("Invalid mobject_or_animation parameter, must be \
subclass or instance of Mobject") subclass or instance of Mobject")
self.starting_mobject = copy.deepcopy(self.mobject)
self.reference_mobjects = [self.starting_mobject] self.reference_mobjects = [self.starting_mobject]
self.alpha_func = alpha_func or (lambda x : x) self.alpha_func = alpha_func or (lambda x : x)
self.run_time = run_time self.run_time = run_time

View File

@ -1,12 +1,12 @@
import os import os
PRODUCTION_QUALITY = True PRODUCTION_QUALITY = False
DEFAULT_POINT_DENSITY_2D = 25 if PRODUCTION_QUALITY else 20 DEFAULT_POINT_DENSITY_2D = 25 #if PRODUCTION_QUALITY else 20
DEFAULT_POINT_DENSITY_1D = 200 if PRODUCTION_QUALITY else 50 DEFAULT_POINT_DENSITY_1D = 200 #if PRODUCTION_QUALITY else 50
HEIGHT = 1024#1440 if PRODUCTION_QUALITY else 480 DEFAULT_HEIGHT = 1440 #if PRODUCTION_QUALITY else 480
WIDTH = 1024#2560 if PRODUCTION_QUALITY else 640 DEFAULT_WIDTH = 2560 #if PRODUCTION_QUALITY else 640
#All in seconds #All in seconds
DEFAULT_FRAME_DURATION = 0.04 if PRODUCTION_QUALITY else 0.1 DEFAULT_FRAME_DURATION = 0.04 if PRODUCTION_QUALITY else 0.1
DEFAULT_ANIMATION_RUN_TIME = 3.0 DEFAULT_ANIMATION_RUN_TIME = 3.0
@ -15,22 +15,28 @@ DEFAULT_DITHER_TIME = 1.0
GENERALLY_BUFF_POINTS = PRODUCTION_QUALITY GENERALLY_BUFF_POINTS = PRODUCTION_QUALITY
BACKGROUND_COLOR = "black" #TODO, this is never actually enforced anywhere.
DEFAULT_NUM_STARS = 1000 DEFAULT_NUM_STARS = 1000
SPACE_HEIGHT = 4.0 SPACE_HEIGHT = 4.0
SPACE_WIDTH = WIDTH * SPACE_HEIGHT / HEIGHT SPACE_WIDTH = DEFAULT_WIDTH * SPACE_HEIGHT / DEFAULT_HEIGHT
PDF_DENSITY = 400
IMAGE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "images")
GIF_DIR = os.path.join(os.getenv("HOME"), "Desktop", "math_gifs") THIS_DIR = os.path.dirname(os.path.realpath(__file__))
MOVIE_DIR = os.path.join(os.getenv("HOME"), "Desktop", "math_movies") IMAGE_DIR = os.path.join(THIS_DIR, "images")
PDF_DIR = os.path.join(os.getenv("HOME"), "Documents", "Tex", "Animations") GIF_DIR = os.path.join(THIS_DIR, "gifs")
MOVIE_DIR = os.path.join(THIS_DIR, "movies")
TEX_DIR = os.path.join(THIS_DIR, "Tex")
TEX_IMAGE_DIR = os.path.join(IMAGE_DIR, "Tex")
TMP_IMAGE_DIR = "/tmp/animation_images/" TMP_IMAGE_DIR = "/tmp/animation_images/"
for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TMP_IMAGE_DIR]: for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TEX_DIR, TMP_IMAGE_DIR, TEX_IMAGE_DIR]:
if not os.path.exists(folder): if not os.path.exists(folder):
os.mkdir(folder) os.mkdir(folder)
PDF_DENSITY = 400
SIZE_TO_REPLACE = "SizeHere"
TEX_TEXT_TO_REPLACE = "YourTextHere"
TEMPLATE_TEX_FILE = os.path.join(TEX_DIR, "template.tex")
LOGO_PATH = os.path.join(IMAGE_DIR, "logo.png") LOGO_PATH = os.path.join(IMAGE_DIR, "logo.png")

View File

@ -2,39 +2,47 @@ import numpy as np
import itertools as it import itertools as it
import os import os
from PIL import Image from PIL import Image
import subprocess
import cv2 import cv2
from animate import * from animate import *
def paint_mobject(mobject, image_array = None):
if image_array is None:
pixels = np.zeros((DEFAULT_HEIGHT, DEFAULT_WIDTH, 3))
else:
pixels = np.array(image_array)
assert len(pixels.shape) == 3 and pixels.shape[2] == 3
height = pixels.shape[0]
width = pixels.shape[1]
space_height = SPACE_HEIGHT
space_width = SPACE_HEIGHT * width / height
def get_image(points, rgbs):
return Image.fromarray(get_pixels(points, rgbs))
def get_pixels(points, rgbs):
#TODO, Let z add a depth componenet? #TODO, Let z add a depth componenet?
points = points[:, :2] points = np.array(mobject.points[:, :2])
#Flips y-axis #Flips y-axis
points[:,1] *= -1 points[:,1] *= -1
#Map points to pixel space, then create pixel array first in terms #Map points to pixel space, then create pixel array first in terms
#of its flattened version #of its flattened version
points += np.array( points += np.array(
[SPACE_WIDTH, SPACE_HEIGHT]*points.shape[0] [space_width, space_height]*points.shape[0]
).reshape(points.shape) ).reshape(points.shape)
points *= np.array( points *= np.array(
[HEIGHT / (2.0 * SPACE_HEIGHT), WIDTH / (2.0 * SPACE_WIDTH)]*\ [width / (2.0 * space_width), height / (2.0 * space_height)]*\
points.shape[0] points.shape[0]
).reshape(points.shape) ).reshape(points.shape)
points = points.astype('int') points = points.astype('int')
flattener = np.array([1, WIDTH], dtype = 'int').reshape((2, 1)) flattener = np.array([1, width], dtype = 'int').reshape((2, 1))
indices = np.dot(points, flattener) indices = np.dot(points, flattener)
indices = indices.reshape(indices.size) indices = indices.reshape(indices.size)
admissibles = (indices < HEIGHT * WIDTH) * (indices > 0) admissibles = (indices < height * width) * (indices > 0)
indices = indices[admissibles] indices = indices[admissibles]
rgbs = rgbs[admissibles] rgbs = mobject.rgbs[admissibles]
rgbs = (rgbs * 255).astype(int) rgbs = (rgbs * 255).astype(int)
pixels = np.zeros((HEIGHT * WIDTH, 3)) pixels = pixels.reshape((height * width, 3))
pixels[indices] = rgbs pixels[indices] = rgbs
return pixels.reshape((HEIGHT, WIDTH, 3)).astype('uint8') pixels = pixels.reshape((height, width, 3)).astype('uint8')
return pixels
def write_to_gif(scene, name): def write_to_gif(scene, name):
#TODO, find better means of compression #TODO, find better means of compression
@ -43,29 +51,31 @@ def write_to_gif(scene, name):
filepath = os.path.join(GIF_DIR, name) filepath = os.path.join(GIF_DIR, name)
temppath = os.path.join(GIF_DIR, "Temp.gif") temppath = os.path.join(GIF_DIR, "Temp.gif")
print "Writing " + name + "..." print "Writing " + name + "..."
writeGif(temppath, scene.frames, scene.frame_duration) images = [Image.fromarray(frame) for frame in scene.frames]
writeGif(temppath, images, scene.frame_duration)
print "Compressing..." print "Compressing..."
os.system("gifsicle -O " + temppath + " > " + filepath) os.system("gifsicle -O " + temppath + " > " + filepath)
os.system("rm " + temppath) os.system("rm " + temppath)
def write_to_movie(scene, name): def write_to_movie(scene, name):
#TODO, incorporate pause time
frames = scene.frames frames = scene.frames
progress_bar = progressbar.ProgressBar(maxval=len(frames)) progress_bar = progressbar.ProgressBar(maxval=len(frames))
progress_bar.start() progress_bar.start()
print "writing " + name + "..." print "writing " + name + "..."
filepath = os.path.join(MOVIE_DIR, name)
filedir = "/".join(filepath.split("/")[:-1])
if not os.path.exists(filedir):
os.makedirs(filedir)
rate = int(1/scene.frame_duration)
tmp_stem = os.path.join(TMP_IMAGE_DIR, name.replace("/", "_")) tmp_stem = os.path.join(TMP_IMAGE_DIR, name.replace("/", "_"))
suffix = "-%04d.png" suffix = "-%04d.png"
image_files = [] image_files = []
for frame, count in zip(frames, it.count()): for frame, count in zip(frames, it.count()):
progress_bar.update(int(0.9 * count)) progress_bar.update(int(0.9 * count))
frame.save(tmp_stem + suffix%count) Image.fromarray(frame).save(tmp_stem + suffix%count)
image_files.append(tmp_stem + suffix%count) image_files.append(tmp_stem + suffix%count)
filepath = os.path.join(MOVIE_DIR, name + ".mp4")
filedir = "/".join(filepath.split("/")[:-1])
if not os.path.exists(filedir):
os.makedirs(filedir)
commands = [ commands = [
"ffmpeg", "ffmpeg",
"-y", "-y",
@ -77,7 +87,7 @@ def write_to_movie(scene, name):
"libx264", "libx264",
"-vf", "-vf",
"fps=%d,format=yuv420p"%int(1/scene.frame_duration), "fps=%d,format=yuv420p"%int(1/scene.frame_duration),
filepath filepath + ".mp4"
] ]
os.system(" ".join(commands)) os.system(" ".join(commands))
for image_file in image_files: for image_file in image_files:
@ -85,10 +95,17 @@ def write_to_movie(scene, name):
progress_bar.finish() progress_bar.finish()
# vs = VideoSink(scene.shape, filepath, rate)
# for frame in frames:
# vs.run(frame)
# vs.close()
# progress_bar.finish()
# filepath = os.path.join(MOVIE_DIR, name + ".mov") # filepath = os.path.join(MOVIE_DIR, name + ".mov")
# fourcc = cv2.cv.FOURCC(*"8bps") # fourcc = cv2.cv.FOURCC(*"8bps")
# out = cv2.VideoWriter( # out = cv2.VideoWriter(
# filepath, fourcc, 1.0/animation.frame_duration, (WIDTH, HEIGHT), True # filepath, fourcc, 1.0/scene.frame_duration, (DEFAULT_WIDTH, DEFAULT_HEIGHT), True
# ) # )
# progress = 0 # progress = 0
# for frame in frames: # for frame in frames:
@ -103,7 +120,35 @@ def write_to_movie(scene, name):
# progress_bar.finish() # progress_bar.finish()
class VideoSink(object):
def __init__(self, size, filename="output", rate=10, byteorder="bgra") :
self.size = size
cmdstring = [
'mencoder',
'/dev/stdin',
'-demuxer', 'rawvideo',
'-rawvideo', 'w=%i:h=%i'%size[::-1]+":fps=%i:format=%s"%(rate,byteorder),
'-o', filename+'.mp4',
'-ovc', 'lavc',
]
self.p = subprocess.Popen(cmdstring, stdin=subprocess.PIPE, shell=False)
def run(self, image):
"""
Image comes in as HEIGHTxWIDTHx3 numpy array, order rgb
"""
assert image.shape == self.size + (3,)
r, g, b = [image[:,:,i].astype('uint32') for i in range(3)]
a = np.ones(image.shape[:2], dtype = 'uint32')
#hacky
image = sum([
arr << 8**i
for arr, i in zip(range(4), [a, r, g, b])
])
self.p.stdin.write(image.tostring())
def close(self):
self.p.stdin.close()

View File

@ -5,9 +5,10 @@ from PIL import Image
from random import random from random import random
from animate import * from animate import *
from tex_image_utils import NAME_TO_IMAGE_FILE from tex_utils import *
import displayer as disp import displayer as disp
class Mobject(object): class Mobject(object):
""" """
Mathematical Object Mathematical Object
@ -40,10 +41,10 @@ class Mobject(object):
return self.name return self.name
def display(self): def display(self):
disp.get_image(self.points, self.rgbs).show() Image.fromarray(disp.paint_mobject(self)).show()
def save_image(self, name = None): def save_image(self, name = None):
disp.get_image(self.points, self.rgbs).save( Image.fromarray(disp.paint_mobject(self)).save(
os.path.join(MOVIE_DIR, (name or str(self)) + ".png") os.path.join(MOVIE_DIR, (name or str(self)) + ".png")
) )
@ -286,7 +287,7 @@ class Line(Mobject1D):
]) ])
class Cube(Mobject2D): class CubeWithFaces(Mobject2D):
def generate_points(self): def generate_points(self):
self.add_points([ self.add_points([
sgn * np.array(coords) sgn * np.array(coords)
@ -300,7 +301,7 @@ class Cube(Mobject2D):
def unit_normal(self, coords): def unit_normal(self, coords):
return np.array(map(lambda x : 1 if abs(x) == 1 else 0, coords)) return np.array(map(lambda x : 1 if abs(x) == 1 else 0, coords))
class CubeShell(Mobject1D): class Cube(Mobject1D):
DEFAULT_COLOR = "yellow" DEFAULT_COLOR = "yellow"
def generate_points(self): def generate_points(self):
self.add_points([ self.add_points([
@ -449,24 +450,15 @@ class NumberLine(Mobject1D):
for x in np.arange(-self.radius, self.radius, self.interval_size) for x in np.arange(-self.radius, self.radius, self.interval_size)
for y in np.arange(-self.tick_size, self.tick_size, self.epsilon) for y in np.arange(-self.tick_size, self.tick_size, self.epsilon)
]) ])
if self.with_numbers: #TODO, make these numbers a separate object if self.with_numbers:
#TODO, test
vertical_displacement = -0.3 vertical_displacement = -0.3
max_explicit_num = 3 nums = range(-self.radius, self.radius)
num_to_name = dict( nums = map(lambda x : x / self.interval_size, nums)
(x, str(x)) mobs = tex_mobjects(*[str(num) for num in nums])
for x in range(-max_explicit_num, max_explicit_num + 1) for num, mob in zip(nums, mobs):
) mob.center().shift([num, vertical_displacement, 0])
num_to_name[max_explicit_num + 1] = "cdots" self.add(*mobs)
num_to_name[-max_explicit_num - 1] = "cdots"
nums = CompoundMobject(*[
ImageMobject(
NAME_TO_IMAGE_FILE[num_to_name[x]]
).scale(0.6).center().shift(
[x * self.interval_size, vertical_displacement, 0]
)
for x in range(-max_explicit_num - 1, max_explicit_num + 2)
])
self.add_points(nums.points, nums.rgbs)
# class ComplexPlane(Grid): # class ComplexPlane(Grid):
# def __init__(self, *args, **kwargs): # def __init__(self, *args, **kwargs):
@ -529,15 +521,20 @@ class ImageMobject(Mobject2D):
for i in indices for i in indices
], dtype = 'float64') ], dtype = 'float64')
height, width = map(float, (height, width)) height, width = map(float, (height, width))
if height / width > float(HEIGHT) / WIDTH: if height / width > float(DEFAULT_HEIGHT) / DEFAULT_WIDTH:
points *= 2 * SPACE_HEIGHT / height points *= 2 * SPACE_HEIGHT / height
else: else:
points *= 2 * SPACE_WIDTH / width points *= 2 * SPACE_WIDTH / width
self.add_points(points, rgbs = rgbs) self.add_points(points, rgbs = rgbs)
def tex_mobjects(expression, size = "\HUGE"):
images = tex_to_image(expression, size)
if isinstance(images, list):
#TODO, is checking listiness really the best here?
return [ImageMobject(im) for im in images]
else:
return ImageMobject(images)

View File

@ -1,6 +1,7 @@
from PIL import Image from PIL import Image
from colour import Color from colour import Color
import numpy as np import numpy as np
import itertools as it
import warnings import warnings
import time import time
import os import os
@ -16,29 +17,47 @@ import displayer as disp
class Scene(object): class Scene(object):
def __init__(self, def __init__(self,
name = None,
frame_duration = DEFAULT_FRAME_DURATION, frame_duration = DEFAULT_FRAME_DURATION,
name = None): background = None,
height = DEFAULT_HEIGHT,
width = DEFAULT_WIDTH,):
self.frame_duration = frame_duration self.frame_duration = frame_duration
self.frames = [] self.frames = []
self.mobjects = set([]) self.mobjects = []
if background:
self.background = np.array(background)
#TODO, Error checking?
else:
self.background = np.zeros((height, width, 3))
self.shape = self.background.shape[:2]
self.name = name self.name = name
def __str__(self): def __str__(self):
return self.name or "Babadinook" #TODO return self.name or "Babadinook" #TODO
def add(self, *mobjects): def add(self, *mobjects):
#TODO, perhaps mobjects should be ordered, for foreground/background """
self.mobjects.update(mobjects) Mobjects will be displayed, from background to foreground,
in the order with which they are entered.
"""
for mobject in mobjects:
#In case it's already in there, it should
#now be closer to the foreground.
self.remove(mobject)
self.mobjects.append(mobject)
def remove(self, *mobjects): def remove(self, *mobjects):
self.mobjects.difference_update(mobjects) for mobject in mobjects:
while mobject in self.mobjects:
self.mobjects.remove(mobject)
def animate(self, animations, def animate(self, *animations):
dither_time = DEFAULT_DITHER_TIME): #Runtime is determined by the first animation
if isinstance(animations, Animation): run_time = animations[0].run_time
animations = [animations] moving_mobjects = [a.mobject for a in animations]
self.pause(dither_time) self.remove(*moving_mobjects)
run_time = max([anim.run_time for anim in animations]) background = self.get_frame()
print "Generating animations..." print "Generating animations..."
progress_bar = progressbar.ProgressBar(maxval=run_time) progress_bar = progressbar.ProgressBar(maxval=run_time)
@ -46,28 +65,31 @@ class Scene(object):
for t in np.arange(0, run_time, self.frame_duration): for t in np.arange(0, run_time, self.frame_duration):
progress_bar.update(t) progress_bar.update(t)
new_frame = background
for anim in animations: for anim in animations:
anim.update(t) anim.update(t / anim.run_time)
self.frames.append(self.get_frame(*animations)) new_frame = disp.paint_mobject(anim.mobject, new_frame)
self.frames.append(new_frame)
for anim in animations: for anim in animations:
anim.clean_up() anim.clean_up()
self.add(*moving_mobjects)
progress_bar.finish() progress_bar.finish()
def pause(self, duration): def get_frame(self):
frame = self.background
for mob in self.mobjects:
frame = disp.paint_mobject(mob, frame)
return frame
def dither(self, duration = DEFAULT_DITHER_TIME):
self.frames += [self.get_frame()]*int(duration / self.frame_duration) self.frames += [self.get_frame()]*int(duration / self.frame_duration)
def get_frame(self, *animations):
#Include animations so as to display mobjects not in the list
#TODO, This is temporary
mob = list(self.mobjects)[0]
return disp.get_image(mob.points, mob.rgbs)
def write_to_gif(self, name = None, end_dither_time = DEFAULT_DITHER_TIME): def write_to_gif(self, name = None, end_dither_time = DEFAULT_DITHER_TIME):
self.pause(end_dither_time) self.dither(end_dither_time)
disp.write_to_gif(self, name or str(self)) disp.write_to_gif(self, name or str(self))
def write_to_movie(self, name = None, end_dither_time = DEFAULT_DITHER_TIME): def write_to_movie(self, name = None, end_dither_time = DEFAULT_DITHER_TIME):
self.pause(end_dither_time) self.dither(end_dither_time)
disp.write_to_movie(self, name or str(self)) disp.write_to_movie(self, name or str(self))

109
tex_utils.py Normal file
View File

@ -0,0 +1,109 @@
import os
import itertools as it
from PIL import Image
from constants import *
def tex_to_image(expression, size = "\HUGE"):
"""
Returns list of images for correpsonding with a list of expressions
"""
return_list = False
arg = expression
if not isinstance(expression, str):
#TODO, verify that expression is iterable of strings
expression = "\n".join([
"\onslide<%d>{"%count + exp + "}"
for count, exp in zip(it.count(1), expression)
])
return_list = True
filestem = os.path.join(
TEX_DIR, str(hash(expression + size))
)
if not os.path.exists(filestem + ".dvi"):
if not os.path.exists(filestem + ".tex"):
print " ".join([
"Writing ",
"".join(arg),
"at size %s to "%(size),
filestem,
])
with open(TEMPLATE_TEX_FILE, "r") as infile:
body = infile.read()
body = body.replace(SIZE_TO_REPLACE, size)
body = body.replace(TEX_TEXT_TO_REPLACE, expression)
with open(filestem + ".tex", "w") as outfile:
outfile.write(body)
commands = [
"latex",
"-interaction=batchmode",
"-output-directory=" + TEX_DIR,
filestem + ".tex"
]
#TODO, Error check
os.system(" ".join(commands))
assert os.path.exists(filestem + ".dvi")
result = dvi_to_png(filestem + ".dvi")
return result if return_list else result[0]
def dvi_to_png(filename, regen_if_exists = False):
"""
Converts a dvi, which potentially has multiple slides, into a
directory full of enumerated pngs corresponding with these slides.
Returns a list of PIL Image objects for these images sorted as they
where in the dvi
"""
possible_paths = [
filename,
os.path.join(TEX_DIR, filename),
os.path.join(TEX_DIR, filename + ".dvi"),
]
for path in possible_paths:
if os.path.exists(path):
directory, filename = os.path.split(path)
name = filename.split(".")[0]
images_dir = os.path.join(TEX_IMAGE_DIR, name)
if not os.path.exists(images_dir):
os.mkdir(images_dir)
if os.listdir(images_dir) == [] or regen_if_exists:
commands = [
"convert",
"-density",
str(PDF_DENSITY),
path,
"-size",
str(DEFAULT_WIDTH) + "x" + str(DEFAULT_HEIGHT),
os.path.join(images_dir, name + ".png")
]
os.system(" ".join(commands))
image_paths = [
os.path.join(images_dir, name)
for name in os.listdir(images_dir)
if name.endswith(".png")
]
image_paths.sort(cmp_enumerated_files)
return [Image.open(path).convert('RGB') for path in image_paths]
raise IOError("File not Found")
def cmp_enumerated_files(name1, name2):
num1, num2 = [
int(name.split(".")[0].split("-")[-1])
for name in (name1, name2)
]
return num1 - num2