mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
Starting complex number animations
This commit is contained in:
parent
7564beb393
commit
c3cdafcfeb
11 changed files with 314 additions and 63 deletions
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
92
displayer.py
92
displayer.py
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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__":
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
157
scripts/complex_actions.py
Normal 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)
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Reference in a new issue