Maybe 2/3 through inventing math project

This commit is contained in:
Grant Sanderson 2015-08-07 18:10:00 -07:00
parent 9cf8e7b75e
commit 5b228ba8da
14 changed files with 1216 additions and 118 deletions

View file

@ -9,11 +9,34 @@ from mobject import Mobject, Point
from constants import *
from helpers import *
def straight_path(start_points, end_points, alpha):
return (1-alpha)*start_points + alpha*end_points
def semi_circular_path(start_points, end_points, alpha, axis):
midpoints = (start_points + end_points) / 2
angle = alpha * np.pi
rot_matrix = rotation_matrix(angle, axis)[:2, :2]
result = np.zeros(start_points.shape)
result[:,:2] = np.dot(
(start_points - midpoints)[:,:2],
np.transpose(rot_matrix)
) + midpoints[:,:2]
result[:,2] = (1-alpha)*start_points[:,2] + alpha*end_points[:,2]
return result
def clockwise_path(start_points, end_points, alpha):
return semi_circular_path(start_points, end_points, alpha, IN)
def counterclockwise_path(start_points, end_points, alpha):
return semi_circular_path(start_points, end_points, alpha, OUT)
class Transform(Animation):
def __init__(self, mobject1, mobject2,
run_time = DEFAULT_TRANSFORM_RUN_TIME,
interpolation_function = straight_path,
black_out_extra_points = False,
*args, **kwargs):
self.interpolation_function = interpolation_function
count1, count2 = mobject1.get_num_points(), mobject2.get_num_points()
if count2 == 0:
mobject2 = Point((SPACE_WIDTH, SPACE_HEIGHT, 0))
@ -37,10 +60,14 @@ class Transform(Animation):
self.non_redundant_m2_indices = indices
def update_mobject(self, alpha):
Mobject.interpolate(
self.starting_mobject,
self.ending_mobject,
self.mobject,
self.mobject.points = self.interpolation_function(
self.starting_mobject.points,
self.ending_mobject.points,
alpha
)
self.mobject.rgbs = straight_path(
self.starting_mobject.rgbs,
self.ending_mobject.rgbs,
alpha
)
@ -59,23 +86,19 @@ class Transform(Animation):
)[self.non_redundant_m2_indices]
)
class SemiCircleTransform(Transform):
def __init__(self, mobject1, mobject2, counterclockwise = True,
*args, **kwargs):
Transform.__init__(self, mobject1, mobject2, *args, **kwargs)
self.axis = (0, 0, 1) if counterclockwise else (0, 0, -1)
class ClockwiseTransform(Transform):
def __init__(self, mobject1, mobject2, **kwargs):
Transform.__init__(
self, mobject1, mobject2,
interpolation_function = clockwise_path, **kwargs
)
def update_mobject(self, alpha):
sm, em = self.starting_mobject, self.ending_mobject
midpoints = (sm.points + em.points) / 2
angle = alpha * np.pi
rot_matrix = rotation_matrix(angle, self.axis)[:2, :2]
self.mobject.points[:,:2] = np.dot(
(sm.points - midpoints)[:,:2],
np.transpose(rot_matrix)
) + midpoints[:,:2]
self.mobject.points[:,2] = (1-alpha)*sm.points[:,2] + alpha*em.points[:,2]
self.mobject.rgbs = (1-alpha)*sm.rgbs + alpha*em.rgbs
class CounterclockwiseTransform(Transform):
def __init__(self, mobject1, mobject2, **kwargs):
Transform.__init__(
self, mobject1, mobject2,
interpolation_function = counterclockwise_path, **kwargs
)
class FadeToColor(Transform):
def __init__(self, mobject, color, *args, **kwargs):
@ -119,12 +142,25 @@ class ApplyMethod(Transform):
)
class ApplyFunction(Transform):
def __init__(self, mobject, function, run_time = DEFAULT_ANIMATION_RUN_TIME,
*args, **kwargs):
def __init__(self, function, mobject, **kwargs):
Transform.__init__(
self,
mobject,
function(copy.deepcopy(mobject)),
**kwargs
)
self.name = "ApplyFunctionTo"+str(mobject)
class ApplyPointwiseFunction(Transform):
def __init__(self, mobject, function,
run_time = DEFAULT_ANIMATION_RUN_TIME, **kwargs):
map_image = copy.deepcopy(mobject)
map_image.points = np.array(map(function, map_image.points))
Transform.__init__(self, mobject, map_image, run_time = run_time,
*args, **kwargs)
Transform.__init__(
self, mobject, map_image,
run_time = run_time, **kwargs
)
self.name = "".join([
"Apply",
"".join([s.capitalize() for s in function.__name__.split("_")]),

View file

@ -41,8 +41,8 @@ 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))
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")
@ -61,6 +61,7 @@ SIZE_TO_REPLACE = "SizeHere"
TEX_TEXT_TO_REPLACE = "YourTextHere"
TEMPLATE_TEX_FILE = os.path.join(TEX_DIR, "template.tex")
TEMPLATE_TEXT_FILE = os.path.join(TEX_DIR, "text_template.tex")
MAX_LEN_FOR_HUGE_TEX_FONT = 25
LOGO_PATH = os.path.join(IMAGE_DIR, "logo.png")

View file

@ -81,18 +81,26 @@ class VideoIcon(ImageMobject):
self.scale(0.3)
self.center()
def text_mobject(text, size = "\\Large"):
def text_mobject(text, size = None):
size = size or "\\Large" #TODO, auto-adjust?
return tex_mobject(text, size, TEMPLATE_TEXT_FILE)
def tex_mobject(expression,
size = "\\Huge",
size = None,
template_tex_file = TEMPLATE_TEX_FILE):
if size == None:
if len("".join(expression)) < MAX_LEN_FOR_HUGE_TEX_FONT:
size = "\\Huge"
else:
size = "\\large"
#Todo, make this more sophisticated.
images = tex_to_image(expression, size, template_tex_file)
if isinstance(images, list):
#TODO, is checking listiness really the best here?
return CompoundMobject(*map(ImageMobject, images)).center()
result = CompoundMobject(*map(ImageMobject, images))
else:
return ImageMobject(images).center()
result = ImageMobject(images)
return result.highlight("white").center()

View file

@ -146,11 +146,15 @@ class Mobject(object):
center = self.get_center()
return self.center().scale(scale_factor).shift(center)
def stretch(self, factor, dim):
self.points[:,dim] *= factor
return self
def stretch_to_fit(self, length, dim):
center = self.get_center()
old_length = max(self.points[:,dim]) - min(self.points[:,dim])
self.center()
self.points[:,dim] *= length/old_length
self.stretch(length/old_length, dim)
self.shift(center)
return self
@ -178,6 +182,9 @@ class Mobject(object):
return self
def replace(self, mobject, stretch = False):
if mobject.get_num_points() == 0:
raise Warning("Attempting to replace mobject with no points")
return self
if stretch:
self.stretch_to_fit_width(mobject.get_width())
self.stretch_to_fit_height(mobject.get_height())
@ -231,11 +238,34 @@ class Mobject(object):
### Getters ###
def get_num_points(self):
return self.points.shape[0]
return len(self.points)
def get_center(self):
return (np.max(self.points, 0) + np.min(self.points, 0))/2.0
def get_center_of_mass(self):
return np.apply_along_axis(np.mean, 0, self.points)
def get_border_point(self, direction):
return self.points[np.argmax(np.dot(self.points, direction))]
def get_edge_center(self, dim, max_or_min_func):
result = self.get_center()
result[dim] = max_or_min_func(self.points[:,dim])
return result
def get_top(self):
return self.get_edge_center(1, np.max)
def get_bottom(self):
return self.get_edge_center(1, np.min)
def get_right(self):
return self.get_edge_center(0, np.max)
def get_left(self):
return self.get_edge_center(0, np.min)
def get_width(self):
return np.max(self.points[:, 0]) - np.min(self.points[:, 0])

View file

@ -6,6 +6,7 @@ from constants import *
from helpers import *
class Point(Mobject):
DEFAULT_COLOR = "black"
def __init__(self, point = (0, 0, 0), *args, **kwargs):
Mobject.__init__(self, *args, **kwargs)
self.points = np.array(point).reshape(1, 3)
@ -21,16 +22,18 @@ class Arrow(Mobject1D):
length = 1,
tip_length = 0.25,
normal = (0, 0, 1),
density = DEFAULT_POINT_DENSITY_1D,
*args, **kwargs):
self.point = np.array(point)
if tail is not None:
direction = self.point - tail
length = np.linalg.norm(direction)
self.direction = np.array(direction) / np.linalg.norm(direction)
density *= max(length, 0.1)
self.length = length
self.normal = np.array(normal)
self.tip_length = tip_length
Mobject1D.__init__(self, *args, **kwargs)
Mobject1D.__init__(self, density = density, **kwargs)
def generate_points(self):
self.add_points([
@ -131,12 +134,34 @@ class CurvedLine(Line):
class Circle(Mobject1D):
DEFAULT_COLOR = "red"
def __init__(self, radius = 1.0, **kwargs):
self.radius = radius
Mobject1D.__init__(self, **kwargs)
def generate_points(self):
self.add_points([
(np.cos(theta), np.sin(theta), 0)
for theta in np.arange(0, 2 * np.pi, self.epsilon)
(self.radius*np.cos(theta), self.radius*np.sin(theta), 0)
for theta in np.arange(0, 2 * np.pi, self.epsilon/self.radius)
])
class Rectangle(Mobject1D):
DEFAULT_COLOR = "yellow"
def __init__(self, height = 2.0, width = 2.0, **kwargs):
self.height, self.width = height, width
Mobject1D.__init__(self, **kwargs)
def generate_points(self):
wh = [self.width/2.0, self.height/2.0]
self.add_points([
(x, u, 0) if dim==0 else (u, x, 0)
for dim in 0, 1
for u in wh[1-dim], -wh[1-dim]
for x in np.arange(-wh[dim], wh[dim], self.epsilon)
])
class Square(Rectangle):
def __init__(self, side_length = 2.0, **kwargs):
Rectangle.__init__(self, side_length, side_length, **kwargs)
class Bubble(Mobject):
def __init__(self, direction = LEFT, index_of_tip = -1, center = ORIGIN):
@ -151,7 +176,7 @@ class Bubble(Mobject):
return self.points[self.index_of_tip]
def get_bubble_center(self):
return Mobject.get_center(self)+self.center_offset
return self.get_center()+self.center_offset
def move_tip_to(self, point):
self.shift(point - self.get_tip())
@ -168,8 +193,7 @@ class Bubble(Mobject):
def add_content(self, mobject):
mobject.scale(0.75*self.get_width() / mobject.get_width())
mobject.shift(self.get_bubble_center())
self.content = CompoundMobject(self.content, mobject)
self.add(self.content)
self.content = mobject
return self
def write(self, text):
@ -177,10 +201,7 @@ class Bubble(Mobject):
return self
def clear(self):
num_content_points = self.content.points.shape[0]
self.points = self.points[:-num_content_points]
self.rgbs = self.rgbs[:-num_content_points]
self.contents = Mobject()
self.content = Mobject()
return self
class SpeechBubble(Bubble):

View file

@ -10,14 +10,24 @@ from animation import *
from mobject import *
from constants import *
from region import *
from scene import Scene
from scene import Scene, RearrangeEquation
from script_wrapper import command_line_create_scene
class SampleScene(Scene):
class SampleScene(RearrangeEquation):
def construct(self):
tauy = TauCreature()
self.animate(ApplyMethod(tauy.make_sad))
start_terms = "a + b = c".split(" ")
end_terms = "a = c - b + 0".split(" ")
index_map = {
0 : 0,
1 : 3,
2 : 4,
3 : 1,
4 : 2,
}
RearrangeEquation.construct(
self, start_terms, end_terms, index_map
)

View file

@ -1,2 +1,3 @@
from scene import *
from sub_scenes import *
from arithmetic_scenes import *

View file

@ -0,0 +1,93 @@
import numpy as np
import itertools as it
from scene import Scene
from graphs import *
from mobject import *
from animation import *
from region import *
from constants import *
from helpers import *
class RearrangeEquation(Scene):
def construct(
self,
start_terms,
end_terms,
index_map,
size = None,
path = counterclockwise_path,
start_transform = None,
end_transform = None,
leave_start_terms = False,
transform_kwargs = {},
):
transform_kwargs["interpolation_function"] = path
start_mobs, end_mobs = self.get_mobs_from_terms(
start_terms, end_terms, size
)
if start_transform:
start_mobs = start_transform(CompoundMobject(*start_mobs)).split()
if end_transform:
end_mobs = end_transform(CompoundMobject(*end_mobs)).split()
unmatched_start_indices = set(range(len(start_mobs)))
unmatched_end_indices = set(range(len(end_mobs)))
unmatched_start_indices.difference_update(
[n%len(start_mobs) for n in index_map]
)
unmatched_end_indices.difference_update(
[n%len(end_mobs) for n in index_map.values()]
)
mobject_pairs = [
(start_mobs[a], end_mobs[b])
for a, b in index_map.iteritems()
]+ [
(Point(end_mobs[b].get_center()), end_mobs[b])
for b in unmatched_end_indices
]
if not leave_start_terms:
mobject_pairs += [
(start_mobs[a], Point(start_mobs[a].get_center()))
for a in unmatched_start_indices
]
self.add(*start_mobs)
if leave_start_terms:
self.add(CompoundMobject(*start_mobs))
self.dither()
self.animate(*[
Transform(*pair, **transform_kwargs)
for pair in mobject_pairs
])
self.dither()
def get_mobs_from_terms(self, start_terms, end_terms, size):
"""
Need to ensure that all image mobjects for a tex expression
stemming from the same string are point-for-point copies of one
and other. This makes transitions much smoother, and not look
like point-clouds.
"""
num_start_terms = len(start_terms)
all_mobs = np.array(
tex_mobject(start_terms, size = size).split() + \
tex_mobject(end_terms, size = size).split()
)
all_terms = np.array(start_terms+end_terms)
for term in set(all_terms):
matches = all_terms == term
if sum(matches) > 1:
base_mob = all_mobs[list(all_terms).index(term)]
all_mobs[matches] = [
deepcopy(base_mob).replace(target_mob)
for target_mob in all_mobs[matches]
]
return all_mobs[:num_start_terms], all_mobs[num_start_terms:]

View file

@ -247,7 +247,7 @@ class GraphScene(Scene):
mobject.center()
diameter = max(mobject.get_height(), mobject.get_width())
self.animate(*[
SemiCircleTransform(
CounterclockwiseTransform(
vertex,
deepcopy(mobject).shift(vertex.get_center())
)

View file

@ -213,7 +213,6 @@ class Scene(object):
self.number = num_mob
return self
def get_frame(self):
frame = self.background
for mob in self.mobjects:
@ -224,6 +223,10 @@ class Scene(object):
self.frames += [self.get_frame()]*int(duration / self.frame_duration)
return self
def repeat(self, num):
self.frames = self.frames*num
return self
def write_to_gif(self, name = None,
end_dither_time = DEFAULT_DITHER_TIME):
self.dither(end_dither_time)

View file

@ -165,7 +165,7 @@ class OldIntroduceGraphs(GraphScene):
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))
CounterclockwiseTransform(vertex, Dot(point))
for vertex, point in zip(self.vertices, self.points)
]+[
Transform(ann, line)
@ -558,7 +558,7 @@ class FacebookGraph(GraphScene):
self.annotate_edges(friends)
self.dither()
self.animate(*[
SemiCircleTransform(account, vertex)
CounterclockwiseTransform(account, vertex)
for account, vertex in zip(accounts, self.vertices)
])
self.dither()
@ -1176,7 +1176,7 @@ class FinalSum(Scene):
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))
anims.append(CounterclockwiseTransform(copy, mob))
self.clear()
self.animate(*anims, run_time = 2.0)
self.dither()

File diff suppressed because it is too large Load diff

View file

@ -226,7 +226,7 @@ class HardProblemsSimplerQuestions(Scene):
self.remove(*self.mobjects)
self.add(fermat["n"])
self.animate(*[
SemiCircleTransform(mobs[0], mobs[1])
CounterclockwiseTransform(mobs[0], mobs[1])
for f_copy, sym in zip(copies, ["3", "2"])
for mobs in zip(f_copy.split(), fermat[sym].split())
])
@ -260,7 +260,7 @@ class HardProblemsSimplerQuestions(Scene):
self.animate(
FadeIn(fermat2_jargon),
FadeIn(fermat3_jargon),
SemiCircleTransform(start_line, end_line),
CounterclockwiseTransform(start_line, end_line),
ShowCreation(dots)
)
self.dither()
@ -686,17 +686,17 @@ class GraphsAndEulersFormulaJoke(Scene):
self.add(axes)
self.animate(ShowCreation(graph), run_time = 1.0)
eulers = tex_mobject("e^{\pi i} = -1").shift((0, 3, 0))
self.animate(SemiCircleTransform(
self.animate(CounterclockwiseTransform(
deepcopy(graph), eulers
))
self.dither()
self.remove(*self.mobjects)
self.add(eulers)
self.animate(SemiCircleTransform(
self.animate(CounterclockwiseTransform(
CompoundMobject(axes, graph),
Point((-SPACE_WIDTH, SPACE_HEIGHT, 0))
))
self.animate(SemiCircleTransform(
self.animate(CounterclockwiseTransform(
eulers,
Point((SPACE_WIDTH, SPACE_HEIGHT, 0))
))
@ -899,7 +899,7 @@ class ShowMoserGraphLines(CircleScene):
compound = CompoundMobject(*mobs)
if mobs in (self.dots, self.intersection_dots):
self.remove(*mobs)
self.animate(SemiCircleTransform(
self.animate(CounterclockwiseTransform(
compound,
deepcopy(compound).scale(1.05),
alpha_func = there_and_back,
@ -1055,7 +1055,7 @@ class ApplyEulerToMoser(CircleScene):
)
self.animate(*[
SemiCircleTransform(d[1], d[2], run_time = 2.0)
CounterclockwiseTransform(d[1], d[2], run_time = 2.0)
for d in [V, minus, E, plus, F, equals, two]
])
self.dither()
@ -1164,8 +1164,8 @@ class ApplyEulerToMoser(CircleScene):
self.dither()
self.remove(*self.mobjects)
self.animate(
SemiCircleTransform(two[6], two[7]),
SemiCircleTransform(plus[6], plus[7]),
CounterclockwiseTransform(two[6], two[7]),
CounterclockwiseTransform(plus[6], plus[7]),
*[
Transform(d[6], d[7])
for d in [F, equals, nc2, plus1, nc4]
@ -1184,7 +1184,7 @@ class ApplyEulerToMoser(CircleScene):
one = tex_mobject("1").shift(two.get_center())
two.highlight("red")
self.add(two)
self.animate(SemiCircleTransform(two, one))
self.animate(CounterclockwiseTransform(two, one))
class FormulaRelatesToPowersOfTwo(Scene):
def __init__(self, *args, **kwargs):
@ -1226,7 +1226,7 @@ class FormulaRelatesToPowersOfTwo(Scene):
self.remove(*self.mobjects)
self.add(*forms + sums + results)
self.animate(*[
SemiCircleTransform(result, pof2)
CounterclockwiseTransform(result, pof2)
for result, pof2 in zip(results, powers_of_two)
])
@ -1277,7 +1277,7 @@ class PascalsTriangleWithNChooseK(PascalsTriangleScene):
self.dither()
self.remove(*self.mobjects)
self.animate(*[
SemiCircleTransform(
CounterclockwiseTransform(
deepcopy(mob_dicts[i][n][k]),
mob_dicts[1-i][n][k]
)
@ -1399,7 +1399,7 @@ class PascalsTriangleSumRows(PascalsTriangleScene):
self.remove(*to_remove)
self.add(*powers_of_two)
for n in range(self.nrows):
self.animate(SemiCircleTransform(
self.animate(CounterclockwiseTransform(
powers_of_two[n], powers_of_two_symbols[n],
run_time = run_time
))
@ -1456,14 +1456,14 @@ class MoserSolutionInPascal(PascalsTriangleScene):
])
self.animate(*
[
SemiCircleTransform(
CounterclockwiseTransform(
self.coords_to_n_choose_k[n0][k0],
self.coords_to_mobs[n0][k0]
)
for n0, k0 in self.coords
if (n0, k0) not in [(n, k) for k in term_range]
]+[
SemiCircleTransform(terms[k], target_terms[k])
CounterclockwiseTransform(terms[k], target_terms[k])
for k in term_range
]
)
@ -1565,7 +1565,7 @@ class ExplainNChoose2Formula(Scene):
b_mob = nums.pop(b-2 if a < b else b-1)
self.add(b_mob)
self.animate(*[
SemiCircleTransform(
CounterclockwiseTransform(
mob,
Point(mob.get_center()).highlight("black")
)
@ -1582,8 +1582,8 @@ class ExplainNChoose2Formula(Scene):
b_copy = deepcopy(b_mob).center().shift(a_center - (0, 2, 0))
self.add(over_2, deepcopy(a_mob), deepcopy(b_mob))
self.animate(
SemiCircleTransform(a_mob, a_copy),
SemiCircleTransform(b_mob, b_copy),
CounterclockwiseTransform(a_mob, a_copy),
CounterclockwiseTransform(b_mob, b_copy),
FadeIn(parens_copy),
FadeIn(text_mobject("is considered the same as"))
)
@ -1635,7 +1635,7 @@ class ExplainNChoose4Formula(Scene):
)
else:
self.animate(*[
SemiCircleTransform(
CounterclockwiseTransform(
mob,
Point(mob.get_center()).highlight("black")
)
@ -1658,7 +1658,7 @@ class ExplainNChoose4Formula(Scene):
for i in range(4)
]
compound_quad = CompoundMobject(*quad_mobs)
self.animate(SemiCircleTransform(
self.animate(CounterclockwiseTransform(
compound_quad,
CompoundMobject(*new_quad_mobs)
))

View file

@ -203,12 +203,12 @@ class TauPoem(Scene):
)
self.add(two_pi)
self.dither()
self.animate(SemiCircleTransform(
self.animate(CounterclockwiseTransform(
two_pi, sphere,
alpha_func = lambda t : 2*high_inflection_0_to_1(t/2)
))
self.remove(two_pi)
self.animate(SemiCircleTransform(
self.animate(CounterclockwiseTransform(
sphere, tau,
alpha_func = lambda t : 2*(high_inflection_0_to_1(t/2+0.5)-0.5)
))
@ -302,10 +302,10 @@ class TauPoem(Scene):
self.dither()
self.remove(bubble)
bubble_copy = deepcopy(bubble)
self.animate(SemiCircleTransform(bubble_copy, heart))
self.animate(CounterclockwiseTransform(bubble_copy, heart))
self.dither()
self.remove(bubble_copy)
self.animate(SemiCircleTransform(heart, bubble))
self.animate(CounterclockwiseTransform(heart, bubble))
self.dither()
@ -323,7 +323,7 @@ class TauPoem(Scene):
Point(pi.right_leg.points[0,:]).highlight("black")
),
Transform(pi.mouth, tau.mouth),
SemiCircleTransform(
CounterclockwiseTransform(
two,
Dot(two.get_center()).highlight("black")
)
@ -412,8 +412,8 @@ class TauPoem(Scene):
ShowCreation(circle)
)
self.animate(
SemiCircleTransform(sine_period, tau_line),
SemiCircleTransform(circle, deepcopy(tau_line)),
CounterclockwiseTransform(sine_period, tau_line),
CounterclockwiseTransform(circle, deepcopy(tau_line)),
FadeOut(axes),
FadeOut(grid),
FadeOut(sine),
@ -435,7 +435,7 @@ class TauPoem(Scene):
self.add(*formula)
self.dither()
self.animate(SemiCircleTransform(two_pi, tau))
self.animate(CounterclockwiseTransform(two_pi, tau))
self.remove(two_pi)
self.animate(BlinkPiCreature(tau))
self.dither()