Transformation article

This commit is contained in:
Grant Sanderson 2015-11-09 10:34:00 -08:00
parent ac930952f1
commit 199c600e36
7 changed files with 189 additions and 60 deletions

View file

@ -30,32 +30,6 @@ class Animation(object):
def __str__(self):
return self.name
def get_points_and_rgbs(self):
"""
It is the responsibility of this class to only emit points within
the space. Returns np array of points and corresponding np array
of rgbs
"""
#TODO, I don't think this should be necessary. This should happen
#under the individual mobjects.
points = self.mobject.points
rgbs = self.mobject.rgbs
#Filters out what is out of bounds.
admissibles = (abs(points[:,0]) < self.restricted_width) * \
(abs(points[:,1]) < self.restricted_height)
for filter_function in self.filter_functions:
admissibles *= ~filter_function(points)
if any(self.spatial_center):
points += self.spatial_center
#Filter out points pushed off the edge
admissibles *= (abs(points[:,0]) < SPACE_WIDTH) * \
(abs(points[:,1]) < SPACE_HEIGHT)
if rgbs.shape[0] < points.shape[0]:
#TODO, this shouldn't be necessary, find what's happening.
points = points[:rgbs.shape[0], :]
admissibles = admissibles[:rgbs.shape[0]]
return points[admissibles, :], rgbs[admissibles, :]
def update(self, alpha):
if alpha < 0:
alpha = 0.0

View file

@ -4,6 +4,7 @@ import itertools as it
from helpers import *
from animation import Animation
from transform import Transform
from mobject import Mobject
class DelayByOrder(Animation):
@ -43,6 +44,7 @@ class TransformAnimations(Transform):
"alpha_func" : squish_alpha_func(smooth)
}
def __init__(self, start_anim, end_anim, **kwargs):
digest_config(self, kwargs, locals())
if "run_time" in kwargs:
self.run_time = kwargs.pop("run_time")
else:

View file

@ -11,14 +11,26 @@ from transform import Transform
class Rotating(Animation):
DEFAULT_CONFIG = {
"axes" : [RIGHT, UP],
"axis" : None,
"radians" : 2*np.pi,
"run_time" : 20.0,
"alpha_func" : None,
"in_place" : True,
}
def update_mobject(self, alpha):
self.mobject.points = self.starting_mobject.points
for axis in self.axes:
self.mobject.rotate(self.radians * alpha, axis)
axes = [self.axis] if self.axis is not None else self.axes
families = [
self.mobject.get_full_submobject_family(),
self.starting_mobject.get_full_submobject_family()
]
for mob, start in zip(*families):
mob.points = np.array(start.points)
if self.in_place:
method = self.mobject.rotate_in_place
else:
method = self.mobject.rotate
method(alpha*self.radians, axes = axes)
class FadeOut(Animation):
def update_mobject(self, alpha):

View file

@ -103,9 +103,17 @@ class ApplyMethod(Transform):
)
class Rotate(ApplyMethod):
def __init__(self, mobject, angle = np.pi, **kwargs):
DEFAULT_CONFIG = {
"in_place" : False,
}
def __init__(self, mobject, angle = np.pi, axis = OUT, **kwargs):
kwargs["interpolation_function"] = path_along_arc(angle)
ApplyMethod.__init__(self, mobject.rotate, angle, **kwargs)
digest_config(self, kwargs, locals())
if self.in_place:
method = mobject.rotate_in_place
else:
method = mobject.rotate
ApplyMethod.__init__(self, method, angle, axis, **kwargs)
class ApplyPointwiseFunction(ApplyMethod):

View file

@ -120,12 +120,17 @@ class Mobject(object):
mob.points *= scale_factor
return self
def rotate(self, angle, axis = OUT):
t_rotation_matrix = np.transpose(rotation_matrix(angle, axis))
def rotate(self, angle, axis = OUT, axes = []):
if len(axes) == 0:
axes = [axis]
rot_matrix = np.identity(self.DIM)
for axis in axes:
rot_matrix = np.dot(rot_matrix, rotation_matrix(angle, axis))
t_rot_matrix = np.transpose(rot_matrix)
for mob in self.get_full_submobject_family():
mob.points = np.dot(mob.points, t_rotation_matrix)
mob.points = np.dot(mob.points, t_rot_matrix)
if mob.has_normals:
mob.unit_normals = np.dot(mob.unit_normals, t_rotation_matrix)
mob.unit_normals = np.dot(mob.unit_normals, t_rot_matrix)
return self
def stretch(self, factor, dim):
@ -165,6 +170,8 @@ class Mobject(object):
def filter_out(self, condition):
for mob in self.get_full_submobject_family():
if len(mob.points) == 0:
continue
to_eliminate = ~np.apply_along_axis(condition, 1, mob.points)
mob.points = mob.points[to_eliminate]
mob.rgbs = mob.rgbs[to_eliminate]
@ -205,8 +212,8 @@ class Mobject(object):
self.shift(center)
return self
def rotate_in_place(self, angle, axis = OUT):
self.do_in_place(self.rotate, angle, axis)
def rotate_in_place(self, angle, axis = OUT, axes = []):
self.do_in_place(self.rotate, angle, axis, axes)
return self
def scale_in_place(self, scale_factor):
@ -314,13 +321,10 @@ class Mobject(object):
return 0
def get_merged_array(self, array_attr):
return reduce(
lambda a1, a2 : np.append(a1, a2, axis = 0),
[getattr(self, array_attr)] + [
mob.get_merged_array(array_attr)
for mob in self.sub_mobjects
]
)
result = np.zeros((0, self.DIM))
for mob in self.get_full_submobject_family():
result = np.append(result, getattr(mob, array_attr), 0)
return result
def get_all_points(self):
return self.get_merged_array("points")

View file

@ -229,6 +229,7 @@ class XYZAxes(Mobject1D):
self.z_axis = self.x_axis.copy().rotate(np.pi/2, DOWN)
self.digest_mobject_attrs()
self.pose_at_angle()
class SpaceGrid(Mobject1D):
@ -246,6 +247,7 @@ class SpaceGrid(Mobject1D):
start = np.array([a, b, -self.radius])[perm]
end = np.array([a, b, self.radius])[perm]
self.add_line(start, end)
self.pose_at_angle()

View file

@ -7,7 +7,9 @@ def half_plane():
x_radius = SPACE_WIDTH/2,
x_unit_to_spatial_width = 0.5,
y_unit_to_spatial_height = 0.5,
density = 2*DEFAULT_POINT_DENSITY_1D,
x_faded_line_frequency = 0,
y_faded_line_frequency = 0,
density = 4*DEFAULT_POINT_DENSITY_1D,
)
plane.add_coordinates(
x_vals = range(-6, 7, 2),
@ -71,12 +73,12 @@ class SingleVariableFunction(Scene):
class LineToPlaneFunction(Scene):
args_list = [
(lambda x : (np.cos(x), 0.5*x*np.sin(x)), "Swirl", {}),
(lambda x : (np.cos(x), 0.5*x*np.sin(x)), "Swirl", {
"0" : 0,
"\\frac{\\pi}{2}" : np.pi/2,
"\\pi" : np.pi
})
(lambda x : (np.cos(x), 0.5*x*np.sin(x)), "Swirl", []),
(lambda x : (np.cos(x), 0.5*x*np.sin(x)), "Swirl", [
("0", "(1, 0)", 0),
("\\frac{\\pi}{2}", "(0, \\pi / 4)", np.pi/2),
("\\pi", "(-1, 0)", np.pi),
])
]
@staticmethod
@ -97,6 +99,10 @@ class LineToPlaneFunction(Scene):
line.add_numbers(*range(0, 14, 2))
divider = Line(SPACE_HEIGHT*UP, SPACE_HEIGHT*DOWN)
plane = half_plane()
plane.sub_mobjects = []
plane.filter_out(
lambda (x, y, z) : abs(x) > 0.1 and abs(y) > 0.1
)
plane.shift(0.5*SPACE_WIDTH*RIGHT)
self.add(line, divider, plane)
@ -110,7 +116,7 @@ class LineToPlaneFunction(Scene):
anims = [Transform(line_copy, target, **anim_config)]
colors = iter([BLUE_B, GREEN_D, RED_D])
for tex, number in numbers_to_follow.items():
for input_tex, output_tex, number in numbers_to_follow:
center = line.number_to_point(number)
dot = Dot(center, color = colors.next())
anims.append(ApplyMethod(
@ -118,7 +124,7 @@ class LineToPlaneFunction(Scene):
point_function(center) - center,
**anim_config
))
label = TexMobject(tex)
label = TexMobject(input_tex)
label.shift(center + 2*UP)
arrow = Arrow(label, dot)
self.add(label)
@ -131,6 +137,17 @@ class LineToPlaneFunction(Scene):
self.play(*anims)
self.dither()
for input_tex, output_tex, number in numbers_to_follow:
point = plane.num_pair_to_point(func(number))
label = TexMobject(output_tex)
side_shift = LEFT if number == np.pi else RIGHT
label.shift(point, 2*UP, side_shift)
arrow = Arrow(label, point)
self.add(label)
self.play(ShowCreation(arrow))
self.dither(2)
self.remove(arrow, label)
class PlaneToPlaneFunctionSeparatePlanes(Scene):
args_list = [
(lambda (x, y) : (x**2+y**2, x**2-y**2), "Quadratic")
@ -185,9 +202,119 @@ class PlaneToPlaneFunction(Scene):
self.play(ApplyPointwiseFunction(point_function, plane, **anim_config))
self.dither(3)
class PlaneToLineFunction(Scene):
args_list = [
(lambda (x, y) : x**2 + y**2, "Bowl"),
]
@staticmethod
def args_to_string(func, name):
return name
def construct(self, func, name):
line = NumberLine(
color = GREEN,
unit_length_to_spatial_width = 0.5,
tick_frequency = 1,
number_at_center = 6,
numerical_radius = 6,
numbers_with_elongated_ticks = [0, 12],
).to_edge(RIGHT)
line.add_numbers()
plane = half_plane().to_edge(LEFT, buff = 0)
divider = Line(SPACE_HEIGHT*UP, SPACE_HEIGHT*DOWN)
line_left = line.number_to_point(0)
def point_function(point):
shifter = 0.5*SPACE_WIDTH*RIGHT
return func((point+shifter)[:2])*RIGHT + line_left
self.add(line, plane, divider)
self.dither()
plane.sub_mobjects = []
self.play(ApplyPointwiseFunction(point_function, plane))
self.dither()
class PlaneToSpaceFunction(Scene):
args_list = [
(lambda (x, y) : (x*x, x*y, y*y), "Quadratic"),
]
@staticmethod
def args_to_string(func, name):
return name
def construct(self, func, name):
plane = half_plane().shift(0.5*SPACE_WIDTH*LEFT)
divider = Line(SPACE_HEIGHT*UP, SPACE_HEIGHT*DOWN)
axes = XYZAxes()
axes.filter_out(lambda p : np.linalg.norm(p) > 3)
rot_kwargs = {
"run_time" : 3,
"radians" : 0.3*np.pi,
"axis" : [0.1, 1, 0.1],
}
axes.to_edge(RIGHT).shift(DOWN)
dampening_factor = 0.1
def point_function((x, y, z)):
return dampening_factor*np.array(func((x, y)))
target = NumberPlane().apply_function(point_function)
target.highlight("yellow")
target.shift(axes.get_center())
self.add(plane, divider, axes)
self.play(Rotating(axes, **rot_kwargs))
target.rotate_in_place(rot_kwargs["radians"])
self.play(
TransformAnimations(
Animation(plane.copy()),
Rotating(target, **rot_kwargs),
alpha_func = smooth
),
Rotating(axes, **rot_kwargs)
)
axes.add(target)
self.clear()
self.add(plane, divider, axes)
self.play(Rotating(axes, **rot_kwargs))
self.clear()
for i in range(5):
self.play(Rotating(axes, **rot_kwargs))
class SpaceToSpaceFunction(Scene):
args_list = [
(lambda (x, y, z) : (y*z, x*z, x*y), "Quadratic"),
]
@staticmethod
def args_to_string(func, name):
return name
def construct(self, func, name):
space = SpaceGrid()
rot_kwargs = {
"run_time" : 10,
"radians" : 2*np.pi/5,
"axis" : [0.1, 1, 0.1],
"in_place" : False,
}
axes = XYZAxes()
target = space.copy().apply_function(func)
self.play(
TransformAnimations(
Rotating(space, **rot_kwargs),
Rotating(target, **rot_kwargs),
alpha_func = squish_alpha_func(smooth, 0.3, 0.7)
),
Rotating(axes, **rot_kwargs)
)
axes.add(space)
self.play(Rotating(axes, **rot_kwargs))