mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
543 lines
18 KiB
Python
543 lines
18 KiB
Python
#!/usr/bin/env python
|
|
|
|
import numpy as np
|
|
import itertools as it
|
|
from copy import deepcopy
|
|
import sys
|
|
|
|
from helpers import *
|
|
|
|
from scene import Scene
|
|
from number_line import NumberLineScene
|
|
|
|
ARROW_CONFIG = {"point_thickness" : 2*DEFAULT_POINT_THICKNESS}
|
|
LIGHT_RED = RED_E
|
|
|
|
def matrix_to_string(matrix):
|
|
return "--".join(["-".join(map(str, row)) for row in matrix])
|
|
|
|
def matrix_mobject(matrix):
|
|
return TextMobject(
|
|
"""
|
|
\\left(
|
|
\\begin{array}{%s}
|
|
%d & %d \\\\
|
|
%d & %d
|
|
\\end{array}
|
|
\\right)
|
|
"""%tuple(["c"*matrix.shape[1]] + list(matrix.flatten())),
|
|
size = "\\Huge"
|
|
)
|
|
|
|
class ShowMultiplication(NumberLineScene):
|
|
args_list = [
|
|
(2, True),
|
|
(0.5, True),
|
|
(-3, True),
|
|
(-3, True),
|
|
(2, True),
|
|
(6, True),
|
|
]
|
|
@staticmethod
|
|
def args_to_string(num, show_original_line):
|
|
end_string = "WithCopiedOriginalLine" if show_original_line else ""
|
|
return str(num) + end_string
|
|
@staticmethod
|
|
def string_to_args(string):
|
|
parts = string.split()
|
|
if len(parts) == 2:
|
|
num, original_line = parts
|
|
show_original_line = original_line == "WithCopiedOriginalLine"
|
|
return float(num), False
|
|
else:
|
|
return float(parts[0]), False
|
|
|
|
def construct(self, num, show_original_line):
|
|
config = {
|
|
"density" : max(abs(num), 1)*DEFAULT_POINT_DENSITY_1D,
|
|
"point_thickness" : 2*DEFAULT_POINT_THICKNESS
|
|
}
|
|
if abs(num) < 1:
|
|
config["numerical_radius"] = SPACE_WIDTH/num
|
|
|
|
NumberLineScene.construct(self, **config)
|
|
if show_original_line:
|
|
self.copy_original_line()
|
|
self.dither()
|
|
self.show_multiplication(num, run_time = 1.5)
|
|
self.dither()
|
|
|
|
def copy_original_line(self):
|
|
copied_line = deepcopy(self.number_line)
|
|
copied_num_mobs = deepcopy(self.number_mobs)
|
|
self.play(
|
|
ApplyFunction(
|
|
lambda m : m.shift(DOWN).highlight("lightgreen"),
|
|
copied_line
|
|
), *[
|
|
ApplyMethod(mob.shift, DOWN)
|
|
for mob in copied_num_mobs
|
|
]
|
|
)
|
|
self.dither()
|
|
|
|
class ExamplesOfOneDimensionalLinearTransforms(ShowMultiplication):
|
|
args_list = []
|
|
@staticmethod
|
|
def args_to_string():
|
|
return ""
|
|
|
|
def construct(self):
|
|
for num in [2, 0.5, -3]:
|
|
self.clear()
|
|
ShowMultiplication.construct(self, num, False)
|
|
|
|
|
|
|
|
class ExamplesOfNonlinearOneDimensionalTransforms(NumberLineScene):
|
|
def construct(self):
|
|
def sinx_plux_x((x, y, z)):
|
|
return (np.sin(x) + 1.2*x, y, z)
|
|
def shift_zero((x, y, z)):
|
|
return (2*x+4, y, z)
|
|
self.nonlinear = TextMobject("Not a Linear Transform")
|
|
self.nonlinear.highlight(LIGHT_RED).to_edge(UP, buff = 1.5)
|
|
pairs = [
|
|
(sinx_plux_x, "numbers don't remain evenly spaced"),
|
|
(shift_zero, "zero does not remain fixed")
|
|
]
|
|
for func, explanation in pairs:
|
|
self.run_function(func, explanation)
|
|
self.dither(3)
|
|
|
|
def run_function(self, function, explanation):
|
|
self.clear()
|
|
self.add(self.nonlinear)
|
|
config = {
|
|
"point_thickness" : 2*DEFAULT_POINT_THICKNESS,
|
|
"density" : 5*DEFAULT_POINT_DENSITY_1D,
|
|
}
|
|
NumberLineScene.construct(self, **config)
|
|
words = TextMobject(explanation).highlight(LIGHT_RED)
|
|
words.next_to(self.nonlinear, DOWN, buff = 0.5)
|
|
self.add(words)
|
|
|
|
self.play(
|
|
ApplyPointwiseFunction(function, self.number_line),
|
|
*[
|
|
ApplyMethod(
|
|
mob.shift,
|
|
function(mob.get_center()) - mob.get_center()
|
|
)
|
|
for mob in self.number_mobs
|
|
],
|
|
run_time = 2.0
|
|
)
|
|
|
|
|
|
class ShowTwoThenThree(ShowMultiplication):
|
|
args_list = []
|
|
@staticmethod
|
|
def args_to_string():
|
|
return ""
|
|
|
|
def construct(self):
|
|
config = {
|
|
"point_thickness" : 2*DEFAULT_POINT_THICKNESS,
|
|
"density" : 6*DEFAULT_POINT_DENSITY_1D,
|
|
}
|
|
NumberLineScene.construct(self, **config)
|
|
self.copy_original_line()
|
|
self.show_multiplication(2)
|
|
self.dither()
|
|
self.show_multiplication(3)
|
|
self.dither()
|
|
|
|
|
|
########################################################
|
|
|
|
class TransformScene2D(Scene):
|
|
def add_number_plane(self, density_factor = 1, use_faded_lines = True):
|
|
config = {
|
|
"x_radius" : 2*SPACE_WIDTH,
|
|
"y_radius" : 2*SPACE_WIDTH,
|
|
"density" : DEFAULT_POINT_DENSITY_1D*density_factor,
|
|
"point_thickness" : 2*DEFAULT_POINT_THICKNESS
|
|
}
|
|
if not use_faded_lines:
|
|
config["x_faded_line_frequency"] = None
|
|
config["y_faded_line_frequency"] = None
|
|
self.number_plane = NumberPlane(**config)
|
|
self.add(self.number_plane)
|
|
|
|
def add_background(self):
|
|
grey_plane = NumberPlane(color = "grey")
|
|
num_mobs = grey_plane.get_coordinate_labels()
|
|
self.paint_into_background(grey_plane, *num_mobs)
|
|
|
|
def add_x_y_arrows(self):
|
|
self.x_arrow = Arrow(
|
|
ORIGIN,
|
|
self.number_plane.num_pair_to_point((1, 0)),
|
|
color = "lightgreen",
|
|
**ARROW_CONFIG
|
|
)
|
|
self.y_arrow = Arrow(
|
|
ORIGIN,
|
|
self.number_plane.num_pair_to_point((0, 1)),
|
|
color = LIGHT_RED,
|
|
**ARROW_CONFIG
|
|
)
|
|
self.add(self.x_arrow, self.y_arrow)
|
|
self.number_plane.filter_out(
|
|
lambda (x, y, z) : (0 < x) and (x < 1) and (abs(y) < 0.1)
|
|
)
|
|
self.number_plane.filter_out(
|
|
lambda (x, y, z) : (0 < y) and (y < 1) and (abs(x) < 0.1)
|
|
)
|
|
return self
|
|
|
|
|
|
class ShowMatrixTransform(TransformScene2D):
|
|
args_list = [
|
|
([[1, 3], [-2, 0]], False, False),
|
|
([[1, 3], [-2, 0]], True, False),
|
|
([[1, 0.5], [0.5, 1]], True, False),
|
|
([[2, 0], [0, 2]], True, False),
|
|
([[0.5, 0], [0, 0.5]], True, False),
|
|
([[-1, 0], [0, -1]], True, False),
|
|
([[0, 1], [1, 0]], True, False),
|
|
([[-2, 0], [-1, -1]], True, False),
|
|
([[0, -1], [1, 0]], True, False),
|
|
]
|
|
@staticmethod
|
|
def args_to_string(matrix, with_background, show_matrix):
|
|
background_string = "WithBackground" if with_background else "WithoutBackground"
|
|
show_string = "ShowingMatrix" if show_matrix else ""
|
|
return matrix_to_string(matrix) + background_string + show_string
|
|
|
|
def construct(self, matrix, with_background, show_matrix):
|
|
matrix = np.array(matrix)
|
|
number_plane_config = {
|
|
"density_factor" : self.get_density_factor(matrix)
|
|
}
|
|
if with_background:
|
|
self.add_background()
|
|
number_plane_config["use_faded_lines"] = False
|
|
self.add_number_plane(**number_plane_config)
|
|
self.add_x_y_arrows()
|
|
else:
|
|
self.add_number_plane(**number_plane_config)
|
|
if show_matrix:
|
|
self.add(matrix_mobject(matrix).to_corner(UP+LEFT))
|
|
def func(mobject):
|
|
mobject.points[:, :2] = np.dot(mobject.points[:, :2], np.transpose(matrix))
|
|
return mobject
|
|
|
|
self.dither()
|
|
kwargs = {
|
|
"run_time" : 2.0,
|
|
"path_func" : self.get_path_func(matrix)
|
|
}
|
|
anims = [ApplyFunction(func, self.number_plane, **kwargs)]
|
|
if hasattr(self, "x_arrow") and hasattr(self, "y_arrow"):
|
|
for arrow, index in (self.x_arrow, 0), (self.y_arrow, 1):
|
|
new_arrow = Arrow(
|
|
ORIGIN,
|
|
self.number_plane.num_pair_to_point(matrix[:,index]),
|
|
color = arrow.get_color(),
|
|
**ARROW_CONFIG
|
|
)
|
|
arrow.remove_tip()
|
|
new_arrow.remove_tip()
|
|
Mobject.align_data(arrow, new_arrow)
|
|
arrow.add_tip()
|
|
new_arrow.add_tip()
|
|
anims.append(Transform(arrow, new_arrow, **kwargs))
|
|
self.play(*anims)
|
|
self.dither()
|
|
|
|
def get_density_factor(self, matrix):
|
|
max_norm = max([
|
|
abs(np.linalg.norm(column))
|
|
for column in np.transpose(matrix)
|
|
])
|
|
return max(max_norm, 1)
|
|
|
|
def get_path_func(self, matrix):
|
|
rotational_components = np.array([
|
|
np.log(multiplier*complex(*matrix[:,i])).imag
|
|
for i, multiplier in [(0, 1), (1, complex(0, -1))]
|
|
])
|
|
rotational_components[rotational_components == -np.pi] = np.pi
|
|
return path_along_arc(np.mean(rotational_components))
|
|
|
|
|
|
class ExamplesOfTwoDimensionalLinearTransformations(ShowMatrixTransform):
|
|
args_list = []
|
|
@staticmethod
|
|
def args_to_string():
|
|
return ""
|
|
|
|
def construct(self):
|
|
matrices = [
|
|
[[1, 0.5],
|
|
[0.5, 1]],
|
|
[[0, -1],
|
|
[2, 0]],
|
|
[[1, 3],
|
|
[-2, 0]],
|
|
]
|
|
for matrix in matrices:
|
|
self.clear()
|
|
ShowMatrixTransform.construct(self, matrix, False, False)
|
|
|
|
|
|
class ExamplesOfNonlinearTwoDimensionalTransformations(Scene):
|
|
def construct(self):
|
|
Scene.construct(self)
|
|
def squiggle((x, y, z)):
|
|
return (x+np.sin(y), y+np.cos(x), z)
|
|
def shift_zero((x, y, z)):
|
|
return (2*x + 3*y + 4, -1*x+y+2, z)
|
|
self.nonlinear = TextMobject("Nonlinear Transform")
|
|
self.nonlinear.highlight(LIGHT_RED)
|
|
self.nonlinear.to_edge(UP, buff = 1.5)
|
|
pairs = [
|
|
(squiggle, "lines do not remain straight"),
|
|
(shift_zero, "the origin does not remain fixed")
|
|
]
|
|
self.get_blackness()
|
|
for function, explanation in pairs:
|
|
self.apply_function(function, explanation)
|
|
|
|
|
|
def apply_function(self, function, explanation):
|
|
self.clear()
|
|
config = {
|
|
"x_radius" : 2*SPACE_WIDTH,
|
|
"y_radius" : 2*SPACE_WIDTH,
|
|
"density" : 3*DEFAULT_POINT_DENSITY_1D,
|
|
"point_thickness" : 2*DEFAULT_POINT_THICKNESS
|
|
}
|
|
number_plane = NumberPlane(**config)
|
|
numbers = number_plane.get_coordinate_labels()
|
|
words = TextMobject(explanation)
|
|
words.highlight(LIGHT_RED)
|
|
words.next_to(self.nonlinear, DOWN, buff = 0.5)
|
|
|
|
self.add(number_plane, *numbers)
|
|
self.add(self.blackness, self.nonlinear, words)
|
|
self.dither()
|
|
self.play(
|
|
ApplyPointwiseFunction(function, number_plane),
|
|
*[
|
|
ApplyMethod(
|
|
mob.shift,
|
|
function(mob.get_center())-mob.get_center()
|
|
)
|
|
for mob in numbers
|
|
] + [
|
|
Animation(self.blackness),
|
|
Animation(words),
|
|
Animation(self.nonlinear)
|
|
],
|
|
run_time = 2.0
|
|
)
|
|
self.dither(3)
|
|
|
|
def get_blackness(self):
|
|
vertices = [
|
|
3.5*LEFT+1.05*UP,
|
|
3.5*RIGHT+1.05*UP,
|
|
3.5*RIGHT+2.75*UP,
|
|
3.5*LEFT+2.75*UP,
|
|
]
|
|
|
|
region = region_from_polygon_vertices(*vertices)
|
|
image = disp.paint_region(region, color = WHITE)
|
|
self.blackness = TextMobject("")
|
|
ImageMobject.generate_points_from_image_array(self.blackness, image)
|
|
self.blackness.highlight(BLACK)
|
|
rectangle = Rectangle(width = 7, height=1.7)
|
|
rectangle.highlight(WHITE)
|
|
rectangle.shift(self.blackness.get_center())
|
|
self.blackness.add(rectangle)
|
|
self.blackness.scale_in_place(0.95)
|
|
|
|
|
|
class TrickyExamplesOfNonlinearTwoDimensionalTransformations(Scene):
|
|
def construct(self):
|
|
config = {
|
|
"x_radius" : 1.2*SPACE_WIDTH,
|
|
"y_radius" : 1.2*SPACE_WIDTH,
|
|
"density" : 10*DEFAULT_POINT_DENSITY_1D,
|
|
"point_thickness" : 2*DEFAULT_POINT_THICKNESS
|
|
}
|
|
number_plane = NumberPlane(**config)
|
|
phrase1, phrase2 = TextMobject([
|
|
"These might look like they keep lines straight...",
|
|
"but diagonal lines get curved"
|
|
]).to_edge(UP, buff = 1.5).split()
|
|
phrase2.highlight(LIGHT_RED)
|
|
diagonal = Line(
|
|
DOWN*SPACE_HEIGHT+LEFT*SPACE_WIDTH,
|
|
UP*SPACE_HEIGHT+RIGHT*SPACE_WIDTH,
|
|
density = 10*DEFAULT_POINT_DENSITY_1D
|
|
)
|
|
def sunrise((x, y, z)):
|
|
return ((SPACE_HEIGHT+y)*x, y, z)
|
|
|
|
def squished((x, y, z)):
|
|
return (x + np.sin(x), y+np.sin(y), z)
|
|
|
|
self.get_blackness()
|
|
|
|
self.run_function(sunrise, number_plane, phrase1)
|
|
self.run_function(squished, number_plane, phrase1)
|
|
phrase1.add(phrase2)
|
|
self.add(phrase1)
|
|
self.play(ShowCreation(diagonal))
|
|
self.remove(diagonal)
|
|
number_plane.add(diagonal)
|
|
self.run_function(sunrise, number_plane, phrase1)
|
|
self.run_function(squished, number_plane, phrase1, False)
|
|
|
|
|
|
def run_function(self, function, plane, phrase, remove_plane = True):
|
|
number_plane = deepcopy(plane)
|
|
self.add(number_plane, self.blackness, phrase)
|
|
self.dither()
|
|
self.play(
|
|
ApplyPointwiseFunction(function, number_plane, run_time = 2.0),
|
|
Animation(self.blackness),
|
|
Animation(phrase),
|
|
)
|
|
self.dither(3)
|
|
if remove_plane:
|
|
self.remove(number_plane)
|
|
|
|
def get_blackness(self):
|
|
vertices = [
|
|
4.5*LEFT+1.25*UP,
|
|
4.5*RIGHT+1.25*UP,
|
|
4.5*RIGHT+2.75*UP,
|
|
4.5*LEFT+2.75*UP,
|
|
]
|
|
|
|
region = region_from_polygon_vertices(*vertices)
|
|
image = disp.paint_region(region, color = WHITE)
|
|
self.blackness = TextMobject("")
|
|
ImageMobject.generate_points_from_image_array(self.blackness, image)
|
|
self.blackness.highlight(BLACK)
|
|
rectangle = Rectangle(width = 9, height=1.5)
|
|
rectangle.highlight(WHITE)
|
|
rectangle.shift(self.blackness.get_center())
|
|
self.blackness.add(rectangle)
|
|
self.blackness.scale_in_place(0.95)
|
|
|
|
|
|
############# HORRIBLE! ##########################
|
|
class ShowMatrixTransformWithDot(TransformScene2D):
|
|
args_list = [
|
|
([[1, 3], [-2, 0]], True, False),
|
|
]
|
|
@staticmethod
|
|
def args_to_string(matrix, with_background, show_matrix):
|
|
background_string = "WithBackground" if with_background else "WithoutBackground"
|
|
show_string = "ShowingMatrix" if show_matrix else ""
|
|
return matrix_to_string(matrix) + background_string + show_string
|
|
|
|
def construct(self, matrix, with_background, show_matrix):
|
|
matrix = np.array(matrix)
|
|
number_plane_config = {
|
|
"density_factor" : self.get_density_factor(matrix),
|
|
}
|
|
if with_background:
|
|
self.add_background()
|
|
number_plane_config["use_faded_lines"] = False
|
|
self.add_number_plane(**number_plane_config)
|
|
self.add_x_y_arrows()
|
|
else:
|
|
self.add_number_plane(**number_plane_config)
|
|
if show_matrix:
|
|
self.add(matrix_mobject(matrix).to_corner(UP+LEFT))
|
|
def func(mobject):
|
|
mobject.points[:, :2] = np.dot(mobject.points[:, :2], np.transpose(matrix))
|
|
return mobject
|
|
dot = Dot((-1, 2, 0), color = "yellow")
|
|
self.add(dot)
|
|
x_arrow_copy = deepcopy(self.x_arrow)
|
|
y_arrow_copy = Arrow(LEFT, LEFT+2*UP, color = LIGHT_RED, **ARROW_CONFIG)
|
|
|
|
self.play(ApplyMethod(x_arrow_copy.rotate, np.pi))
|
|
self.play(ShowCreation(y_arrow_copy))
|
|
self.dither()
|
|
self.remove(x_arrow_copy, y_arrow_copy)
|
|
kwargs = {
|
|
"run_time" : 2.0,
|
|
"path_func" : self.get_path_func(matrix)
|
|
}
|
|
anims = [
|
|
ApplyFunction(func, self.number_plane, **kwargs),
|
|
ApplyMethod(dot.shift, func(deepcopy(dot)).get_center()-dot.get_center(), **kwargs),
|
|
]
|
|
if hasattr(self, "x_arrow") and hasattr(self, "y_arrow"):
|
|
for arrow, index in (self.x_arrow, 0), (self.y_arrow, 1):
|
|
new_arrow = Arrow(
|
|
ORIGIN,
|
|
self.number_plane.num_pair_to_point(matrix[:,index]),
|
|
color = arrow.get_color(),
|
|
**ARROW_CONFIG
|
|
)
|
|
arrow.remove_tip()
|
|
new_arrow.remove_tip()
|
|
Mobject.align_data(arrow, new_arrow)
|
|
arrow.add_tip()
|
|
new_arrow.add_tip()
|
|
anims.append(Transform(arrow, new_arrow, **kwargs))
|
|
self.play(*anims)
|
|
self.dither()
|
|
|
|
x_arrow_copy = deepcopy(self.x_arrow)
|
|
y_arrow_copy = Arrow(LEFT+2*UP, 5*RIGHT+2*UP, color = LIGHT_RED, **ARROW_CONFIG)
|
|
self.play(ApplyMethod(x_arrow_copy.rotate, np.pi))
|
|
self.play(ShowCreation(y_arrow_copy))
|
|
self.dither(3)
|
|
self.remove(x_arrow_copy, y_arrow_copy)
|
|
|
|
def get_density_factor(self, matrix):
|
|
max_norm = max([
|
|
abs(np.linalg.norm(column))
|
|
for column in np.transpose(matrix)
|
|
])
|
|
return max(max_norm, 1)
|
|
|
|
def get_path_func(self, matrix):
|
|
rotational_components = [
|
|
sign*np.arccos(matrix[i,i]/np.linalg.norm(matrix[:,i]))
|
|
for i in [0, 1]
|
|
for sign in [((-1)**i)*np.sign(matrix[1-i, i])]
|
|
]
|
|
average_rotation = sum(rotational_components)/2
|
|
if abs(average_rotation) < np.pi / 2:
|
|
return straight_path
|
|
elif average_rotation > 0:
|
|
return counterclockwise_path()
|
|
else:
|
|
return clockwise_path()
|
|
|
|
|
|
class Show90DegreeRotation(TransformScene2D):
|
|
def construct(self):
|
|
self.add_number_plane()
|
|
self.add_background()
|
|
self.add_x_y_arrows()
|
|
|
|
self.dither()
|
|
self.play(*[
|
|
RotationAsTransform(mob, run_time = 2.0)
|
|
for mob in self.number_plane, self.x_arrow, self.y_arrow
|
|
])
|
|
self.dither()
|
|
|