Added DocStrings for all methods in Container, Scene, GraphScene, MovingCameraScene, SceneFileWriter, ThreeDScene, SpecialThreeDScene, ZoomedScene, VectorScene, and LinearTransformationScene. (#1040)

* Added DocStrings for methods in Container and Scene.
Removed 2 unused imports from scene.py.

* Added DocStrings for all methods in GraphScene, MovingCameraScene, SceneFileWriter, ThreeDScene, SpecialThreeDScene and ZoomedScene.

* Added DocStrings for all methods in `VectorScene` and LinearTransformationScene...
...except `position_x_coordinate` and `position_y_coordinate`

Co-authored-by: Aathish Sivasubrahmanian <aathishs@Aathishs-MacBook-Air.local>
This commit is contained in:
Aathish 2020-05-14 11:11:22 +05:30 committed by GitHub
parent cf656e9c21
commit a529a59abf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 2111 additions and 16 deletions

View file

@ -8,13 +8,24 @@ from manimlib.utils.config_ops import digest_config
class Container(object):
"""
Base class for Scenes and Mobjects. Generic container.
"""
def __init__(self, **kwargs):
digest_config(self, kwargs)
def add(self, *items):
"""
Generic method to add items to Container.
Must be implemented by subclasses.
"""
raise Exception(
"Container.add is not implemented; it is up to derived classes to implement")
def remove(self, *items):
"""
Generic method to remove items from Container.
Must be implemented by subclasses.
"""
raise Exception(
"Container.remove is not implemented; it is up to derived classes to implement")

View file

@ -54,6 +54,10 @@ class GraphScene(Scene):
}
def setup(self):
"""
This method is used internally by Manim
to set up the scene for proper use.
"""
self.default_graph_colors_cycle = it.cycle(self.default_graph_colors)
self.left_T_label = VGroup()
@ -62,6 +66,14 @@ class GraphScene(Scene):
self.right_v_line = VGroup()
def setup_axes(self, animate=False):
"""
This method sets up the axes of the graph.
Parameters
----------
animate (bool=False)
Whether or not to animate the setting up of the Axes.
"""
# TODO, once eoc is done, refactor this to be less redundant.
x_num_range = float(self.x_max - self.x_min)
self.space_unit_to_x = self.x_axis_width / x_num_range
@ -135,12 +147,51 @@ class GraphScene(Scene):
self.default_graph_colors = it.cycle(self.default_graph_colors)
def coords_to_point(self, x, y):
"""
The graph is smaller than the scene.
Because of this, coordinates in the scene don't map
to coordinates on the graph.
This method returns a scaled coordinate for the graph,
given cartesian coordinates that correspond to the scene..
Parameters
----------
x : (int,float)
The x value
y : (int,float)
The y value
Returns
-------
np.ndarray
The array of the coordinates.
"""
assert(hasattr(self, "x_axis") and hasattr(self, "y_axis"))
result = self.x_axis.number_to_point(x)[0] * RIGHT
result += self.y_axis.number_to_point(y)[1] * UP
return result
def point_to_coords(self, point):
"""
The scene is smaller than the graph.
Because of this, coordinates in the graph don't map
to coordinates on the scene.
This method returns a scaled coordinate for the scene,
given coordinates that correspond to the graph.
Parameters
----------
point (np.ndarray)
The point on the graph.
Returns
-------
tuple
The coordinates on the scene.
"""
return (self.x_axis.point_to_number(point),
self.y_axis.point_to_number(point))
@ -151,6 +202,33 @@ class GraphScene(Scene):
x_max=None,
**kwargs
):
"""
This method gets a curve to plot on the graph.
Parameters
----------
func : function
The function to plot. It's return value should be
the y-coordinate for a given x-coordinate
color : str
The string of the RGB color of the curve. in Hexadecimal representation.
x_min : (Union[int,float])
The lower x_value from which to plot the curve.
x_max : (Union[int,float])
The higher x_value until which to plot the curve.
**kwargs:
Any valid keyword arguments of ParametricFunction.
Return
------
ParametricFunction
The Parametric Curve for the function passed.
"""
if color is None:
color = next(self.default_graph_colors_cycle)
if x_min is None:
@ -174,17 +252,99 @@ class GraphScene(Scene):
return graph
def input_to_graph_point(self, x, graph):
"""
This method returns a coordinate on the curve
given an x_value and a the graoh-curve for which
the corresponding y value should be found.
Parameters
----------
x (Union[int, float])
The x value for which to find the y value.
graph ParametricFunction
The ParametricFunction object on which
the x and y value lie.
Returns
-------
numpy.nparray
The array of the coordinates on the graph.
"""
return self.coords_to_point(x, graph.underlying_function(x))
def angle_of_tangent(self, x, graph, dx=0.01):
"""
Returns the angle to the x axis of the tangent
to the plotted curve at a particular x-value.
Parameters
----------
x (Union[int, float])
The x value at which the tangent must touch the curve.
graph ParametricFunction
The ParametricFunction for which to calculate the tangent.
dx (Union(float, int =0.01))
The small change in x with which a small change in y
will be compared in order to obtain the tangent.
Returns
-------
float
The angle of the tangent with the x axis.
"""
vect = self.input_to_graph_point(
x + dx, graph) - self.input_to_graph_point(x, graph)
return angle_of_vector(vect)
def slope_of_tangent(self, *args, **kwargs):
"""
Returns the slople of the tangent to the plotted curve
at a particular x-value.
Parameters
----------
x (Union[int, float])
The x value at which the tangent must touch the curve.
graph ParametricFunction
The ParametricFunction for which to calculate the tangent.
dx (Union(float, int =0.01))
The small change in x with which a small change in y
will be compared in order to obtain the tangent.
Returns
-------
float
The slope of the tangent with the x axis.
"""
return np.tan(self.angle_of_tangent(*args, **kwargs))
def get_derivative_graph(self, graph, dx=0.01, **kwargs):
"""
Returns the curve of the derivative of the passed
graph.
Parameters
----------
graph (ParametricFunction)
The graph for which the derivative must be found.
dx (Union(float, int =0.01))
The small change in x with which a small change in y
will be compared in order to obtain the derivative.
**kwargs
Any valid keyword argument of ParametricFunction
Returns
-------
ParametricFuncion
The curve of the derivative.
"""
if "color" not in kwargs:
kwargs["color"] = self.default_derivative_color
@ -201,6 +361,37 @@ class GraphScene(Scene):
buff=MED_SMALL_BUFF,
color=None,
):
"""
This method returns a properly positioned label for the passed graph,
styled with the passed parameters.
Parameters
----------
graph : ParametricFunction
The curve of the function plotted.
label : str = "f(x)"
The label for the function's curve.
x_val : Union[float, int]
The x_value with which the label should be aligned.
direction : Union[np.ndarray,list,tuple]=RIGHT
The position, relative to the curve that the label will be at.
e.g LEFT, RIGHT
buff : Union[float, int]
The buffer space between the curve and the label
color : str
The color of the label.
Returns
-------
TexMobject
The LaTeX of the passed 'label' parameter
"""
label = TexMobject(label)
color = color or graph.get_color()
label.set_color(color)
@ -234,6 +425,61 @@ class GraphScene(Scene):
show_signed_area=True,
width_scale_factor=1.001
):
"""
This method returns the VGroup() of the Riemann Rectangles for
a particular curve.
Parameters
----------
graph (ParametricFunction)
The graph whose area needs to be approximated
by the Riemann Rectangles.
x_min Union[int,float]
The lower bound from which to start adding rectangles
x_max Union[int,float]
The upper bound where the rectangles stop.
dx Union[int,float]
The smallest change in x-values that is
considered significant.
input_sample_type str
Can be any of "left", "right" or "center
stroke_width : Union[int, float]
The stroke_width of the border of the rectangles.
stroke_color : str
The string of hex colour of the rectangle's border.
fill_opacity Union[int, float]
The opacity of the rectangles.
start_color : str,
The hex starting colour for the rectangles,
this will, if end_color is a different colour,
make a nice gradient.
end_color : str,
The hex ending colour for the rectangles,
this will, if start_color is a different colour,
make a nice gradient.
show_signed_area : bool (True)
Whether or not to indicate -ve area if curve dips below
x-axis.
width_scale_factor : Union[int, float]
How much the width of the rectangles are scaled by when transforming.
Returns
-------
VGroup
A VGroup containing the Riemann Rectangles.
"""
x_min = x_min if x_min is not None else self.x_min
x_max = x_max if x_max is not None else self.x_max
if start_color is None:
@ -279,6 +525,37 @@ class GraphScene(Scene):
stroke_width=1,
**kwargs
):
"""
This method returns a list of multiple VGroups of Riemann
Rectangles. The inital VGroups are relatively inaccurate,
but the closer you get to the end the more accurate the Riemann
rectangles become
Parameters
----------
graph (ParametricFunction)
The graph whose area needs to be approximated
by the Riemann Rectangles.
n_iterations,
The number of VGroups of successive accuracy that are needed.
max_dx Union[int,float]
The maximum change in x between two VGroups of Riemann Rectangles
power_base Union[int,float=2]
stroke_width : Union[int, float]
The stroke_width of the border of the rectangles.
**kwargs
Any valid keyword arguments of get_riemann_rectangles.
Returns
-------
list
The list of Riemann Rectangles of increasing accuracy.
"""
return [
self.get_riemann_rectangles(
graph=graph,
@ -290,6 +567,27 @@ class GraphScene(Scene):
]
def get_area(self, graph, t_min, t_max):
"""
Returns a VGroup of Riemann rectangles
sufficiently small enough to visually
approximate the area under the graph passed.
Parameters
----------
graph (ParametricFunction)
The graph/curve for which the area needs to be gotten.
t_min Union[int, float]
The lower bound of x from which to approximate the area.
t_max Union[int, float]
The upper bound of x until which the area must be approximated.
Returns
-------
VGroup
The VGroup containing the Riemann Rectangles.
"""
numerator = max(t_max - t_min, 0.0001)
dx = float(numerator) / self.num_rects
return self.get_riemann_rectangles(
@ -301,6 +599,23 @@ class GraphScene(Scene):
).set_fill(opacity=self.area_opacity)
def transform_between_riemann_rects(self, curr_rects, new_rects, **kwargs):
"""
This method is used to transform between two VGroups of Riemann Rectangles,
if they were obtained by get_riemann_rectangles or get_riemann_rectangles_list.
No animation is returned, and the animation is directly played.
Parameters
----------
curr_rects : VGroup
The current Riemann Rectangles
new_rects : VGroup
The Riemann Rectangles to transform to.
**kwargs
added_anims
Any other animations to play simultaneously.
"""
transform_kwargs = {
"run_time": 2,
"lag_ratio": 0.5
@ -326,6 +641,31 @@ class GraphScene(Scene):
line_class=Line,
**line_kwargs
):
"""
This method returns a Vertical line from the x-axis to
the corresponding point on the graph/curve.
Parameters
----------
x Union[int,float]
The x-value at which the line should be placed/calculated.
graph (ParametricFunction)
The graph on which the line should extend to.
line_class (Line and similar)
The type of line that should be used.
Defaults to Line
**line_kwargs
Any valid keyword arguments of the object passed in "line_class"
If line_class is Line, any valid keyword arguments of Line are allowed.
Return
------
An object of type passed in "line_class"
Defaults to Line
"""
if "color" not in line_kwargs:
line_kwargs["color"] = graph.get_color()
return line_class(
@ -341,6 +681,30 @@ class GraphScene(Scene):
num_lines=20,
**kwargs
):
"""
Obtains multiple lines from the x axis to the Graph/curve.
Parameters
----------
graph (ParametricFunction)
The graph on which the line should extend to.
x_min (Union[int, float])
The lower bound from which lines can appear.
x_max (Union[int, float])
The upper bound until which the lines can appear.
num_lines (Union[int, float])
The number of lines (evenly spaced)
that are needed.
Returns
-------
VGroup
The VGroup of the evenly spaced lines.
"""
x_min = x_min or self.x_min
x_max = x_max or self.x_max
return VGroup(*[
@ -361,14 +725,57 @@ class GraphScene(Scene):
secant_line_length=10,
):
"""
Resulting group is of the form VGroup(
dx_line,
df_line,
dx_label, (if applicable)
df_label, (if applicable)
secant_line, (if applicable)
)
with attributes of those names.
This method returns a VGroup of (two lines
representing dx and df, the labels for dx and
df, and the Secant to the Graph/curve at a
particular x value.
Parameters
----------
x (Union[float, int])
The x value at which the secant enters, and intersects
the graph for the first time.
graph (ParametricFunction)
The curve/graph for which the secant must
be found.
dx (Union[float, int])
The change in x after which the secant exits.
dx_line_color (str)
The line color for the line that indicates the change in x.
df_line_color (str)
The line color for the line that indicates the change in y.
dx_label (str)
The label to be provided for the change in x.
df_label (str)
The label to be provided for the change in y.
include_secant_line (bool=True)
Whether or not to include the secant line in the graph,
or just have the df and dx lines and labels.
secant_line_color (str)
The color of the secant line.
secant_line_length (Union[float,int=10])
How long the secant line should be.
Returns:
--------
VGroup
Resulting group is of the form VGroup(
dx_line,
df_line,
dx_label, (if applicable)
df_label, (if applicable)
secant_line, (if applicable)
)
with attributes of those names.
"""
kwargs = locals()
kwargs.pop("self")
@ -438,6 +845,33 @@ class GraphScene(Scene):
return group
def add_T_label(self, x_val, side=RIGHT, label=None, color=WHITE, animated=False, **kwargs):
"""
This method adds to the Scene:
-- a Vertical line from the x-axis to the corresponding point on the graph/curve.
-- a small vertical Triangle whose top point lies on the base of the vertical line
-- a TexMobject to be a label for the Line and Triangle, at the bottom of the Triangle.
The scene needs to have the graph have the identifier/variable name self.v_graph.
Parameters
----------
x_val (Union[float, int])
The x value at which the secant enters, and intersects
the graph for the first time.
side (np.ndarray())
label (str)
The label to give the vertline and triangle
color (str)
The hex color of the label.
animated (bool=False)
Whether or not to animate the addition of the T_label
**kwargs
Any valid keyword argument of a self.play call.
"""
triangle = RegularPolygon(n=3, start_angle=np.pi / 2)
triangle.set_height(MED_SMALL_BUFF)
triangle.move_to(self.coords_to_point(x_val, 0), UP)
@ -479,6 +913,33 @@ class GraphScene(Scene):
fade_close_to_origin=True,
run_time=1.0
):
"""
This method requires a lot of prerequisites:
self.area must be defined from self.get_area()
self.left_v_line and self.right_v_line must be defined from self.get_v_line
self.left_T_label_group and self.right_T_label_group must be defined from self.add_T_label
This method will returna VGroup of new mobjects for each of those, when provided the graph/curve,
the new t_min and t_max, the run_time and a bool stating whether or not to fade when close to
the origin.
Parameters
----------
graph (ParametricFunction)
The graph for which this must be done.
new_t_min (Union[int,float])
The new lower bound.
new_t_max (Union[int,float])
The new upper bound.
fade_close_to_origin (bool=True)
Whether or not to fade when close to the origin.
run_time (Union[int,float=1.0])
The run_time of the animation of this change.
"""
curr_t_min = self.x_axis.point_to_number(self.area.get_left())
curr_t_max = self.x_axis.point_to_number(self.area.get_right())
if new_t_min is None:
@ -532,6 +993,32 @@ class GraphScene(Scene):
added_anims=None,
**anim_kwargs
):
"""
This method animates the change of the secant slope group from
the old secant slope group, into a new secant slope group.
Parameters
----------
secant_slope_group (VGroup)
The old secant_slope_group
target_dx Union[int, float]
The new dx value.
target_x Union[int, float]
The new x value at which the secant should be.
run_time Union[int,float=3]
The run time for this change when animated.
added_anims
Any exta animations that should be played alongside.
**anim_kwargs
Any valid kwargs of a self.play call.
NOTE: At least one of target_dx and target_x should be not None.
"""
if target_dx is None and target_x is None:
raise Exception(
"At least one of target_x and target_dx must not be None")

View file

@ -4,11 +4,19 @@ from manimlib.utils.iterables import list_update
class MovingCameraScene(Scene):
"""
This is a Scene, with special configurations and properties that
make it suitable for cases where the camera must be moved around.
"""
CONFIG = {
"camera_class": MovingCamera
}
def setup(self):
"""
This method is used internally by Manim
to set up the scene for proper use.
"""
Scene.setup(self)
assert(isinstance(self.camera, MovingCamera))
self.camera_frame = self.camera.frame
@ -17,6 +25,15 @@ class MovingCameraScene(Scene):
return self
def get_moving_mobjects(self, *animations):
"""
This method returns a list of all of the Mobjects in the Scene that
are moving, that are also in the animations passed.
Parameters
----------
*animations (Animation)
The animations whose mobjects will be checked.
"""
moving_mobjects = Scene.get_moving_mobjects(self, *animations)
all_moving_mobjects = self.camera.extract_mobject_family_members(
moving_mobjects

View file

@ -7,18 +7,40 @@ from tqdm import tqdm as ProgressDisplay
import numpy as np
from manimlib.animation.animation import Animation
from manimlib.animation.creation import Write
from manimlib.animation.transform import MoveToTarget, ApplyMethod
from manimlib.camera.camera import Camera
from manimlib.constants import *
from manimlib.container.container import Container
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.svg.tex_mobject import TextMobject
from manimlib.scene.scene_file_writer import SceneFileWriter
from manimlib.utils.iterables import list_update
class Scene(Container):
"""
A Scene can be thought of as the Canvas of your animation.
All of your own named Scenes will be subclasses of this Scene, or
other named scenes.
Use a construct() function to tell Manim what should go on in the Scene.
E.G:
class MyScene(Scene):
def construct(self):
self.play(
Write(Text("Hello World!"))
)
Some important variables to note are:
camera: The camera object to be used for the scene.
file_writer : The object that writes the animations in the scene to a video file.
mobjects : The list of mobjects present in the scene.
foreground_mobjects : List of mobjects explicitly in the foreground.
num_plays : Number of play() functions in the scene.
time: time elapsed since initialisation of scene.
random_seed: The seed with which all random operations are done.
"""
CONFIG = {
"camera_class": Camera,
"camera_config": {},
@ -59,22 +81,35 @@ class Scene(Container):
def setup(self):
"""
This is meant to be implement by any scenes which
This is meant to be implemented by any scenes which
are comonly subclassed, and have some common setup
involved before the construct method is called.
"""
pass
def tear_down(self):
"""
This is meant to be implemented by any scenes which
are comonly subclassed, and have some common method
to be invoked before the scene ends.
"""
pass
def construct(self):
"""
The primary method for constructing (i.e adding content to)
the Scene.
"""
pass # To be implemented in subclasses
def __str__(self):
return self.__class__.__name__
def print_end_message(self):
"""
Used internally to print the number of
animations played after the scene ends.
"""
print("Played {} animations".format(self.num_plays))
def set_variables_as_attrs(self, *objects, **newly_named_objects):
@ -93,37 +128,111 @@ class Scene(Container):
return self
def get_attrs(self, *keys):
"""
Gets attributes of a scene given the attribute's identifier/name.
Parameters
----------
*args: (str)
Name(s) of the argument(s) to return the attribute of.
Returns
-------
list
List of attributes of the passed identifiers.
"""
return [getattr(self, key) for key in keys]
# Only these methods should touch the camera
def set_camera(self, camera):
"""
Sets the scene's camera to be the passed Camera Object.
Parameters
----------
camera: Union[Camera, MappingCamera,MovingCamera,MultiCamera,ThreeDCamera]
Camera object to use.
"""
self.camera = camera
def get_frame(self):
"""
Gets current frame as NumPy array.
Returns
-------
np.array
NumPy array of pixel values of each pixel in screen
"""
return np.array(self.camera.get_pixel_array())
def get_image(self):
"""
Gets current frame as PIL Image
Returns
-------
PIL.Image
PIL Image object of current frame.
"""
return self.camera.get_image()
def set_camera_pixel_array(self, pixel_array):
"""
Sets the camera to display a Pixel Array
Parameters
----------
pixel_array: Union[np.ndarray,list,tuple]
Pixel array to set the camera to display
"""
self.camera.set_pixel_array(pixel_array)
def set_camera_background(self, background):
"""
Sets the camera to display a Pixel Array
Parameters
----------
background: Union[np.ndarray,list,tuple]
"""
self.camera.set_background(background)
def reset_camera(self):
"""
Resets the Camera to its original configuration.
"""
self.camera.reset()
def capture_mobjects_in_camera(self, mobjects, **kwargs):
def capture_mobjects_in_camera(self, mobjects, **kwargs): #TODO Add more detail to docstring.
"""
This method is used internally.
"""
self.camera.capture_mobjects(mobjects, **kwargs)
def update_frame(
def update_frame( #TODO Description in Docstring
self,
mobjects=None,
background=None,
include_submobjects=True,
ignore_skipping=True,
**kwargs):
"""
Parameters:
-----------
mobjects: list
list of mobjects
background: np.ndarray
Pixel Array for Background
include_submobjects: bool (True)
ignore_skipping : bool (True)
**kwargs
"""
if self.skip_animations and not ignore_skipping:
return
if mobjects is None:
@ -146,10 +255,26 @@ class Scene(Container):
###
def update_mobjects(self, dt):
"""
Begins updating all mobjects in the Scene.
Parameters
----------
dt: Union[int,float]
Change in time between updates. Defaults (mostly) to 1/frames_per_second
"""
for mobject in self.mobjects:
mobject.update(dt)
def should_update_mobjects(self):
"""
Returns True if any mobject in Scene is being updated
or if the scene has always_update_mobjects set to true.
Returns
-------
bool
"""
return self.always_update_mobjects or any([
mob.has_time_based_updater()
for mob in self.get_mobject_family_members()
@ -158,14 +283,39 @@ class Scene(Container):
###
def get_time(self):
"""
Returns time in seconds elapsed after initialisation of scene
Returns
-------
self.time : Union[int,float]
Returns time in seconds elapsed after initialisation of scene
"""
return self.time
def increment_time(self, d_time):
"""
Increments the time elapsed after intialisation of scene by
passed "d_time".
Parameters
----------
d_time : Union[int,float]
Time in seconds to increment by.
"""
self.time += d_time
###
def get_top_level_mobjects(self):
"""
Returns all mobjects which are not submobjects.
Returns
-------
list
List of top level mobjects.
"""
# Return only those which are not in the family
# of another mobject from the scene
mobjects = self.get_mobjects()
@ -180,12 +330,34 @@ class Scene(Container):
return list(filter(is_top_level, mobjects))
def get_mobject_family_members(self):
"""
Returns list of family-members of all mobjects in scene.
If a Circle() and a VGroup(Rectangle(),Triangle()) were added,
it returns not only the Circle(), Rectangle() and Triangle(), but
also the VGroup() object.
Returns
-------
list
List of mobject family members.
"""
return self.camera.extract_mobject_family_members(self.mobjects)
def add(self, *mobjects):
"""
Mobjects will be displayed, from background to
foreground in the order with which they are added.
Parameters
---------
*mobjects
Mobjects to add.
Returns
-------
Scene
The same scene after adding the Mobjects in.
"""
mobjects = [*mobjects, *self.foreground_mobjects]
self.restructure_mobjects(to_remove=mobjects)
@ -205,6 +377,11 @@ class Scene(Container):
return self
def remove(self, *mobjects):
"""
Removes mobjects in the passed list of mobjects
from the scene and the foreground, by removing them
from "mobjects" and "foreground_mobjects"
"""
for list_name in "mobjects", "foreground_mobjects":
self.restructure_mobjects(mobjects, list_name, False)
return self
@ -213,10 +390,31 @@ class Scene(Container):
mobject_list_name="mobjects",
extract_families=True):
"""
tl:wr
If your scene has a Group(), and you removed a mobject from the Group,
this dissolves the group and puts the rest of the mobjects directly
in self.mobjects or self.foreground_mobjects.
In cases where the scene contains a group, e.g. Group(m1, m2, m3), but one
of its submobjects is removed, e.g. scene.remove(m1), the list of mobjects
will be editing to contain other submobjects, but not m1, e.g. it will now
will be edited to contain other submobjects, but not m1, e.g. it will now
insert m2 and m3 to where the group once was.
Parameters
----------
to_remove : Mobject
The Mobject to remove.
mobject_list_name : str
The list of mobjects ("mobjects", "foreground_mobjects" etc) to remove from.
extract_families : bool
Whether the mobject's families should be recursively extracted.
Returns
-------
Scene
The Scene mobject with restructured Mobjects.
"""
if extract_families:
to_remove = self.camera.extract_mobject_family_members(to_remove)
@ -226,6 +424,25 @@ class Scene(Container):
return self
def get_restructured_mobject_list(self, mobjects, to_remove):
"""
Given a list of mobjects and a list of mobjects to be removed, this
filters out the removable mobjects from the list of mobjects.
Parameters
----------
mobjects : list
The Mobjects to check.
to_remove : list
The list of mobjects to remove.
Returns
-------
list
The list of mobjects with the mobjects to remove removed.
"""
new_mobjects = []
def add_safe_mobjects_from_list(list_to_examine, set_to_remove):
@ -242,6 +459,20 @@ class Scene(Container):
# TODO, remove this, and calls to this
def add_foreground_mobjects(self, *mobjects):
"""
Adds mobjects to the foreground, and internally to the list
foreground_mobjects, and mobjects.
Parameters
----------
*mobjects : Mobject
The Mobjects to add to the foreground.
Returns
------
Scene
The Scene, with the foreground mobjects added.
"""
self.foreground_mobjects = list_update(
self.foreground_mobjects,
mobjects
@ -250,36 +481,151 @@ class Scene(Container):
return self
def add_foreground_mobject(self, mobject):
"""
Adds a single mobject to the foreground, and internally to the list
foreground_mobjects, and mobjects.
Parameters
----------
mobject : Mobject
The Mobject to add to the foreground.
Returns
------
Scene
The Scene, with the foreground mobject added.
"""
return self.add_foreground_mobjects(mobject)
def remove_foreground_mobjects(self, *to_remove):
"""
Removes mobjects from the foreground, and internally from the list
foreground_mobjects.
Parameters
----------
*to_remove : Mobject
The mobject(s) to remove from the foreground.
Returns
------
Scene
The Scene, with the foreground mobjects removed.
"""
self.restructure_mobjects(to_remove, "foreground_mobjects")
return self
def remove_foreground_mobject(self, mobject):
"""
Removes a single mobject from the foreground, and internally from the list
foreground_mobjects.
Parameters
----------
mobject : Mobject
The mobject to remove from the foreground.
Returns
------
Scene
The Scene, with the foreground mobject removed.
"""
return self.remove_foreground_mobjects(mobject)
def bring_to_front(self, *mobjects):
"""
Adds the passed mobjects to the scene again,
pushing them to he front of the scene.
Parameters
----------
*mobjects : Mobject
The mobject(s) to bring to the front of the scene.
Returns
------
Scene
The Scene, with the mobjects brought to the front
of the scene.
"""
self.add(*mobjects)
return self
def bring_to_back(self, *mobjects):
"""
Removes the mobject from the scene and
adds them to the back of the scene.
Parameters
----------
*mobjects : Mobject
The mobject(s) to push to the back of the scene.
Returns
------
Scene
The Scene, with the mobjects pushed to the back
of the scene.
"""
self.remove(*mobjects)
self.mobjects = list(mobjects) + self.mobjects
return self
def clear(self):
"""
Removes all mobjects present in self.mobjects
and self.foreground_mobjects from the scene.
Returns
------
Scene
The Scene, with all of its mobjects in
self.mobjects and self.foreground_mobjects
removed.
"""
self.mobjects = []
self.foreground_mobjects = []
return self
def get_mobjects(self):
"""
Returns all the mobjects in self.mobjects
Returns
------
list
The list of self.mobjects .
"""
return list(self.mobjects)
def get_mobject_copies(self):
"""
Returns a copy of all mobjects present in
self.mobjects .
Returns
------
list
A list of the copies of all the mobjects
in self.mobjects
"""
return [m.copy() for m in self.mobjects]
def get_moving_mobjects(self, *animations):
"""
Gets all moving mobjects in the passed animation(s).
Parameters
----------
*animations
The animations to check for moving mobjects.
Returns
------
list
The list of mobjects that could be moving in
the Animation(s)
"""
# Go through mobjects from start to end, and
# as soon as there's one that needs updating of
# some kind per frame, return the list from that
@ -297,6 +643,32 @@ class Scene(Container):
return []
def get_time_progression(self, run_time, n_iterations=None, override_skip_animations=False):
"""
You will hardly use this when making your own animations.
This method is for Manim's internal use.
Returns a CommandLine ProgressBar whose fill_time
is dependent on the run_time of an animation,
the iterations to perform in that animation
and a bool saying whether or not to consider
the skipped animations.
Parameters
----------
run_time: Union[int,float]
The run_time of the animation.
n_iterations: None, int
The number of iterations in the animation.
override_skip_animations: bool (True)
Whether or not to show skipped animations in the progress bar.
Returns
------
ProgressDisplay
The CommandLine Progress Bar.
"""
if self.skip_animations and not override_skip_animations:
times = [run_time]
else:
@ -310,9 +682,43 @@ class Scene(Container):
return time_progression
def get_run_time(self, animations):
"""
Gets the total run time for a list of animations.
Parameters
----------
animations: list
A list of the animations whose total
run_time is to be calculated.
Returns
------
float
The total run_time of all of the animations in the list.
"""
return np.max([animation.run_time for animation in animations])
def get_animation_time_progression(self, animations):
"""
You will hardly use this when making your own animations.
This method is for Manim's internal use.
Uses get_time_progression to obtaina
CommandLine ProgressBar whose fill_time is
dependent on the qualities of the passed animation,
Parameters
----------
animations : list
The list of animations to get
the time progression for.
Returns
------
ProgressDisplay
The CommandLine Progress Bar.
"""
run_time = self.get_run_time(animations)
time_progression = self.get_time_progression(run_time)
time_progression.set_description("".join([
@ -332,6 +738,15 @@ class Scene(Container):
s hit, a MoveToTarget animation is built using the args that
follow up until either another animation is hit, another method
is hit, or the args list runs out.
Parameters
----------
*args : Union[Animation, method(of a mobject, which is followed by that method's arguments)]
**kwargs : any named arguments like run_time or lag_ratio.
Returns
-------
list : list of animations with the parameters applied to them.
"""
animations = []
state = {
@ -390,6 +805,14 @@ class Scene(Container):
return animations
def update_skipping_status(self):
"""
This method is used internally to check if the current
animation needs to be skipped or not. It also checks if
the number of animations that were played correspond to
the number of animations that need to be played, and
raises an EndSceneEarlyException if they don't correspond.
"""
if self.start_at_animation_number:
if self.num_plays == self.start_at_animation_number:
self.skip_animations = False
@ -399,6 +822,25 @@ class Scene(Container):
raise EndSceneEarlyException()
def handle_play_like_call(func):
"""
This method is used internally to wrap the
passed function, into a function that
actually writes to the video stream.
Simultaneously, it also adds to the number
of animations played.
Parameters
----------
func: function object
The play() like function that has to be
written to the video file stream.
Returns
-------
function object
The play() like function that can now write
to the video file stream.
"""
def wrapper(self, *args, **kwargs):
self.update_skipping_status()
allow_write = not self.skip_animations
@ -409,6 +851,17 @@ class Scene(Container):
return wrapper
def begin_animations(self, animations):
"""
This method begins the list of animations that is passed,
and adds any mobjects involved (if not already present)
to the scene again.
Parameters
----------
animations: list
List of involved animations.
"""
curr_mobjects = self.get_mobject_family_members()
for animation in animations:
# Begin animation
@ -421,6 +874,15 @@ class Scene(Container):
curr_mobjects += mob.get_family()
def progress_through_animations(self, animations):
"""
This method progresses through each animation
in the list passed and and updates the frames as required.
Parameters
----------
animations: list
List of involved animations.
"""
# Paint all non-moving objects onto the screen, so they don't
# have to be rendered every frame
moving_mobjects = self.get_moving_mobjects(*animations)
@ -439,6 +901,15 @@ class Scene(Container):
self.add_frames(self.get_frame())
def finish_animations(self, animations):
"""
This function cleans up after the end
of each animation in the passed list.
Parameters
----------
animations: list
list of animations to finish.
"""
for animation in animations:
animation.finish()
animation.clean_up_from_scene(self)
@ -453,6 +924,16 @@ class Scene(Container):
@handle_play_like_call
def play(self, *args, **kwargs):
"""
This method is used to prep the animations for rendering,
apply the arguments and parameters required to them,
render them, and write them to the video file.
Parameters
----------
*args: Animation, mobject with mobject method and params
**kwargs: named parameters affecting what was passed in *args e.g run_time, lag_ratio etc.
"""
if len(args) == 0:
warnings.warn("Called Scene.play with no animations")
return
@ -464,19 +945,70 @@ class Scene(Container):
self.finish_animations(animations)
def idle_stream(self):
"""
This method is used internally to
idle the vide file_writer until an
animation etc needs to be written
to the video file.
"""
self.file_writer.idle_stream()
def clean_up_animations(self, *animations):
"""
This method cleans up and removes from the
scene all the animations that were passed
Parameters
----------
*animations: Animation
Animation to clean up.
Returns
-------
Scene
The scene with the animations
cleaned up.
"""
for animation in animations:
animation.clean_up_from_scene(self)
return self
def get_mobjects_from_last_animation(self):
"""
This method returns the mobjects from the previous
played animation, if any exist, and returns an empty
list if not.
Returns
--------
list
The list of mobjects from the previous animation.
"""
if hasattr(self, "mobjects_from_last_animation"):
return self.mobjects_from_last_animation
return []
def get_wait_time_progression(self, duration, stop_condition):
"""
This method is used internally to obtain the CommandLine
Progressbar for when self.wait() is called in a scene.
Parameters
----------
duration: Union[list,float]
duration of wait time
stop_condition: function
The function which determines whether to continue waiting.
Returns
-------
ProgressBar
The CommandLine ProgressBar of the wait time
"""
if stop_condition is not None:
time_progression = self.get_time_progression(
duration,
@ -495,6 +1027,23 @@ class Scene(Container):
@handle_play_like_call
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
"""
This method is used to wait, and do nothing to the scene, for some
duration.
Updaters stop updating, nothing happens.
Parameters
----------
duration : Union[float, int]
The duration of wait time. Defaults to None.
stop_condition :
A function that determines whether to stop waiting or not.
Returns
-------
Scene
The scene, after waiting.
"""
self.update_mobjects(dt=0) # Any problems with this?
if self.should_update_mobjects():
time_progression = self.get_wait_time_progression(duration, stop_condition)
@ -522,19 +1071,62 @@ class Scene(Container):
return self
def wait_until(self, stop_condition, max_time=60):
"""
Like a wrapper for wait().
You pass a function that determines whether to continue waiting,
and a max wait time if that is never fulfilled.
Parameters
----------
stop_condition: function definition
The function whose boolean return value determines whether to continue waiting
max_time: Union[int,float]
The maximum wait time in seconds, if the stop_condition is never fulfilled.
Defaults to 60.
"""
self.wait(max_time, stop_condition=stop_condition)
def force_skipping(self):
"""
This forces the skipping of animations,
by setting original_skipping_status to
whatever skip_animations was, and setting
skip_animations to True.
Returns
-------
Scene
The Scene, with skipping turned on.
"""
self.original_skipping_status = self.skip_animations
self.skip_animations = True
return self
def revert_to_original_skipping_status(self):
"""
Forces the scene to go back to its original skipping status,
by setting skip_animations to whatever it reads
from original_skipping_status.
Returns
-------
Scene
The Scene, with the original skipping status.
"""
if hasattr(self, "original_skipping_status"):
self.skip_animations = self.original_skipping_status
return self
def add_frames(self, *frames):
"""
Adds a frame to the video_file_stream
Parameters
----------
*frames : numpy.ndarray
The frames to add, as pixel arrays.
"""
dt = 1 / self.camera.frame_rate
self.increment_time(len(frames) * dt)
if self.skip_animations:
@ -543,12 +1135,32 @@ class Scene(Container):
self.file_writer.write_frame(frame)
def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs):
"""
This method is used to add a sound to the animation.
Parameters
----------
sound_file: str
The path to the sound file.
time_offset: int,float = 0
The offset in the sound file after which
the sound can be played.
gain:
**kwargs : Present for excess?
"""
if self.skip_animations:
return
time = self.get_time() + time_offset
self.file_writer.add_sound(sound_file, time, gain, **kwargs)
def show_frame(self):
"""
Opens the current frame in the Default Image Viewer
of your system.
"""
self.update_frame(ignore_skipping=True)
self.get_image().show()

View file

@ -20,6 +20,21 @@ from manimlib.utils.sounds import get_full_sound_file_path
class SceneFileWriter(object):
"""
SceneFileWriter is the object that actually writes the animations
played, into video files, using FFMPEG, and Sox, if sound is needed.
This is mostly for Manim's internal use. You will rarely, if ever,
have to use the methods for this class, unless tinkering with the very
fabric of Manim's reality.
Some useful attributes are:
"write_to_movie" (bool=False)
Whether or not to write the animations into a video file.
"png_mode" (str="RGBA")
The PIL image mode to use when outputting PNGs
"movie_file_extension" (str=".mp4")
The file-type extension of the outputted video.
"""
CONFIG = {
"write_to_movie": False,
# TODO, save_pngs is doing nothing
@ -44,6 +59,11 @@ class SceneFileWriter(object):
# Output directories and files
def init_output_directories(self):
"""
This method initialises the directories to which video
files will be written to and read from (within MEDIA_DIR).
If they don't already exist, they will be created.
"""
module_directory = self.output_directory or self.get_default_module_directory()
scene_name = self.file_name or self.get_default_scene_name()
if self.save_last_frame:
@ -90,17 +110,58 @@ class SceneFileWriter(object):
))
def get_default_module_directory(self):
"""
This method gets the name of the directory containing
the file that has the Scene that is being rendered.
Returns
-------
str
The name of the directory.
"""
filename = os.path.basename(self.input_file_path)
root, _ = os.path.splitext(filename)
return root
def get_default_scene_name(self):
"""
This method returns the default scene name
which is the value of "file_name", if it exists and
the actual name of the class that inherited from
Scene in your animation script, if "file_name" is None.
Returns
-------
str
The default scene name.
"""
if self.file_name is None:
return self.scene.__class__.__name__
else:
return self.file_name
def get_resolution_directory(self):
"""
This method gets the name of the directory that immediately contains the
video file. This name is <height_in_pixels_of_video>p<frame_rate>
E.G:
If you are rendering an 854x480 px animation at 15fps, the name of the directory
that immediately contains the video file will be
480p15.
The file structure should look something like:
MEDIA_DIR
|--Tex
|--texts
|--videos
|--<name_of_file_containing_scene>
|--<height_in_pixels_of_video>p<frame_rate>
|--<scene_name>.mp4
Returns
-------
str
The name of the directory.
"""
pixel_height = self.scene.camera.pixel_height
frame_rate = self.scene.camera.frame_rate
return "{}p{}".format(
@ -109,9 +170,32 @@ class SceneFileWriter(object):
# Directory getters
def get_image_file_path(self):
"""
This returns the directory path to which any images will be
written to.
It is usually named "images", but can be changed by changing
"image_file_path".
Returns
-------
str
The path of the directory.
"""
return self.image_file_path
def get_next_partial_movie_path(self):
"""
Manim renders each play-like call in a short partial
video file. All such files are then concatenated with
the help of FFMPEG.
This method returns the path of the next partial movie.
Returns
-------
str
The path of the next partial movie.
"""
result = os.path.join(
self.partial_movie_directory,
"{:05}{}".format(
@ -122,18 +206,46 @@ class SceneFileWriter(object):
return result
def get_movie_file_path(self):
"""
Returns the final path of the written video file.
Returns
-------
str
The path of the movie file.
"""
return self.movie_file_path
# Sound
def init_audio(self):
"""
Preps the writer for adding audio to the movie.
"""
self.includes_sound = False
def create_audio_segment(self):
"""
Creates an empty, silent, Audio Segment.
"""
self.audio_segment = AudioSegment.silent()
def add_audio_segment(self, new_segment,
time=None,
gain_to_background=None):
"""
This method adds an audio segment from an
AudioSegment type object and suitable parameters.
Parameters
----------
new_segment (AudioSegment)
The audio segment to add
time (Union[int, float])
the timestamp at which the
sound should be added.
gain_to_background
The gain of the segment from the background.
"""
if not self.includes_sound:
self.includes_sound = True
self.create_audio_segment()
@ -158,6 +270,25 @@ class SceneFileWriter(object):
)
def add_sound(self, sound_file, time=None, gain=None, **kwargs):
"""
This method adds an audio segment from a sound file.
Parameters
----------
sound_file (str)
The path to the sound file.
time (Union[float, int])
The timestamp at which the audio should be added.
gain
The gain of the given audio segment.
**kwargs
This method uses add_audio_segment, so any keyword arguments
used there can be referenced here.
"""
file_path = get_full_sound_file_path(sound_file)
new_segment = AudioSegment.from_file(file_path)
if gain:
@ -166,23 +297,62 @@ class SceneFileWriter(object):
# Writers
def begin_animation(self, allow_write=False):
"""
Used internally by manim to stream the animation to FFMPEG for
displaying or writing to a file.
Parameters
----------
allow_write (bool=False)
Whether or not to write to a video file.
"""
if self.write_to_movie and allow_write:
self.open_movie_pipe()
def end_animation(self, allow_write=False):
"""
Internally used by Manim to stop streaming to
FFMPEG gracefully.
Parameters
----------
allow_write (bool=False)
Whether or not to write to a video file.
"""
if self.write_to_movie and allow_write:
self.close_movie_pipe()
def write_frame(self, frame):
"""
Used internally by Manim to write a frame to
the FFMPEG input buffer.
Parameters
----------
frame (np.ndarray)
Pixel array of the frame.
"""
if self.write_to_movie:
self.writing_process.stdin.write(frame.tostring())
def save_final_image(self, image):
"""
The name is a misnomer. This method saves the image
passed to it as an in the default image directory.
Parameters
----------
image (np.ndarray)
The pixel array of the image to save.
"""
file_path = self.get_image_file_path()
image.save(file_path)
self.print_file_ready_message(file_path)
def idle_stream(self):
"""
Doesn't write anything to the FFMPEG frame buffer.
"""
while self.stream_lock:
a = datetime.datetime.now()
self.update_frame()
@ -196,6 +366,13 @@ class SceneFileWriter(object):
sleep(frame_duration - time_diff)
def finish(self):
"""
Finishes writing to the FFMPEG buffer.
Combines the partial movie files into the
whole scene.
If save_last_frame is True, saves the last
frame in the default image directory.
"""
if self.write_to_movie:
if hasattr(self, "writing_process"):
self.writing_process.terminate()
@ -205,6 +382,11 @@ class SceneFileWriter(object):
self.save_final_image(self.scene.get_image())
def open_movie_pipe(self):
"""
Used internally by Manim to initalise
FFMPEG and begin writing to FFMPEG's input
buffer.
"""
file_path = self.get_next_partial_movie_path()
temp_file_path = os.path.splitext(file_path)[0] + '_temp' + self.movie_file_extension
@ -243,6 +425,11 @@ class SceneFileWriter(object):
self.writing_process = subprocess.Popen(command, stdin=subprocess.PIPE)
def close_movie_pipe(self):
"""
Used internally by Manim to gracefully stop writing to FFMPEG's
input buffer, and move the temporary files into their permananant
locations
"""
self.writing_process.stdin.close()
self.writing_process.wait()
shutil.move(
@ -251,6 +438,11 @@ class SceneFileWriter(object):
)
def combine_movie_files(self):
"""
Used internally by Manim to combine the separate
partial movie files that make up a Scene into a single
video file for that Scene.
"""
# Manim renders the scene as many smaller movie files
# which are then concatenated to a larger one. The reason
# for this is that sometimes video-editing is made easier when
@ -339,4 +531,7 @@ class SceneFileWriter(object):
self.print_file_ready_message(movie_file_path)
def print_file_ready_message(self, file_path):
"""
Prints the "File Ready" message to STDOUT.
"""
print("\nFile ready at {}\n".format(file_path))

View file

@ -13,6 +13,10 @@ from manimlib.utils.config_ops import merge_dicts_recursively
class ThreeDScene(Scene):
"""
This is a Scene, with special configurations and properties that
make it suitable for Three Dimensional Scenes.
"""
CONFIG = {
"camera_class": ThreeDCamera,
"ambient_camera_rotation": None,
@ -23,6 +27,23 @@ class ThreeDScene(Scene):
}
def set_camera_orientation(self, phi=None, theta=None, distance=None, gamma=None):
"""
This method sets the orientation of the camera in the scene.
Parameters
----------
phi : (int,float)
The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians.
theta : (int,float)
The azimuthal angle i.e the angle that spins the camera around the Z_AXIS.
distance : (int, float)
The radial distance between ORIGIN and Camera.
gamma : (int, float)
The rotation of the camera about the vector from the ORIGIN to the Camera.
"""
if phi is not None:
self.camera.set_phi(phi)
if theta is not None:
@ -33,6 +54,16 @@ class ThreeDScene(Scene):
self.camera.set_gamma(gamma)
def begin_ambient_camera_rotation(self, rate=0.02):
"""
This method begins an ambient rotation of the camera about the Z_AXIS,
in the anticlockwise direction
Parameters
----------
rate : (int,float=0.02)
The rate at which the camera should rotate about the Z_AXIS.
Negative rate means clockwise rotation.
"""
# TODO, use a ValueTracker for rate, so that it
# can begin and end smoothly
self.camera.theta_tracker.add_updater(
@ -41,6 +72,9 @@ class ThreeDScene(Scene):
self.add(self.camera.theta_tracker)
def stop_ambient_camera_rotation(self):
"""
This method stops all ambient camera rotation.
"""
self.camera.theta_tracker.clear_updaters()
self.remove(self.camera.theta_tracker)
@ -52,6 +86,31 @@ class ThreeDScene(Scene):
frame_center=None,
added_anims=[],
**kwargs):
"""
This method animates the movement of the camera
to the given spherical coordinates.
Parameters
----------
phi : (int,float)
The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians.
theta : (int,float)
The azimuthal angle i.e the angle that spins the camera around the Z_AXIS.
distance : (int, float)
The radial distance between ORIGIN and Camera.
gamma : (int, float)
The rotation of the camera about the vector from the ORIGIN to the Camera.
frame_center : Union[list,tuple,array]
The new center of the camera frame.
added_anims : list
Any other animations to be played at the same time?
"""
anims = []
value_tracker_pairs = [
(phi, self.camera.phi_tracker),
@ -73,6 +132,15 @@ class ThreeDScene(Scene):
self.play(*anims + added_anims)
def get_moving_mobjects(self, *animations):
"""
This method returns a list of all of the Mobjects in the Scene that
are moving, that are also in the animations passed.
Parameters
----------
*animations (Animation)
The animations whose mobjects will be checked.
"""
moving_mobjects = Scene.get_moving_mobjects(self, *animations)
camera_mobjects = self.camera.get_value_trackers()
if any([cm in moving_mobjects for cm in camera_mobjects]):
@ -80,27 +148,96 @@ class ThreeDScene(Scene):
return moving_mobjects
def add_fixed_orientation_mobjects(self, *mobjects, **kwargs):
"""
This method is used to prevent the rotation and tilting
of mobjects as the camera moves around. The mobject can
still move in the x,y,z directions, but will always be
at the angle (relative to the camera) that it was at
when it was passed through this method.)
Parameters
----------
*mobjects (Mobjects)
The Mobjects whose orientation must be fixed.
**kwargs
Some valid kwargs are
use_static_center_func (bool)
center_func (function)
"""
self.add(*mobjects)
self.camera.add_fixed_orientation_mobjects(*mobjects, **kwargs)
def add_fixed_in_frame_mobjects(self, *mobjects):
"""
This method is used to prevent the rotation and movement
of mobjects as the camera moves around. The mobject is
essentially overlayed, and is not impacted by the camera's
movement in any way.
Parameters
----------
*mobjects (Mobjects)
The Mobjects whose orientation must be fixed.
"""
self.add(*mobjects)
self.camera.add_fixed_in_frame_mobjects(*mobjects)
def remove_fixed_orientation_mobjects(self, *mobjects):
"""
This method "unfixes" the orientation of the mobjects
passed, meaning they will no longer be at the same angle
relative to the camera. This only makes sense if the
mobject was passed through add_fixed_orientation_mobjects first.
Parameters
----------
*mobjects (Mobjects)
The Mobjects whose orientation must be unfixed.
"""
self.camera.remove_fixed_orientation_mobjects(*mobjects)
def remove_fixed_in_frame_mobjects(self, *mobjects):
"""
This method undoes what add_fixed_in_frame_mobjects does.
It allows the mobject to be affected by the movement of
the camera.
Parameters
----------
*mobjects (Mobjects)
The Mobjects whose position and orientation must be unfixed.
"""
self.camera.remove_fixed_in_frame_mobjects(*mobjects)
##
def set_to_default_angled_camera_orientation(self, **kwargs):
"""
This method sets the default_angled_camera_orientation to the
keyword arguments passed, and sets the camera to that orientation.
Parameters
----------
**kwargs
Some recognised kwargs are phi, theta, distance, gamma,
which have the same meaning as the parameters in set_camera_orientation.
"""
config = dict(self.default_camera_orientation_kwargs)
config.update(kwargs)
self.set_camera_orientation(**config)
class SpecialThreeDScene(ThreeDScene):
"""
This is basically ThreeDScene++ .
It has some extra configuration for
axes, spheres, lower quality etc.
Some key differences are:
The camera shades applicable 3DMobjects by default,
except if rendering in low quality.
Some default params for Spheres and Axes have been added.
"""
CONFIG = {
"cut_axes_at_radius": True,
"camera_config": {
@ -149,6 +286,13 @@ class SpecialThreeDScene(ThreeDScene):
ThreeDScene.__init__(self, **config)
def get_axes(self):
"""
Returns a set of 3D Axes.
Returns
-------
ThreeDAxes object
"""
axes = ThreeDAxes(**self.three_d_axes_config)
for axis in axes:
if self.cut_axes_at_radius:
@ -170,13 +314,39 @@ class SpecialThreeDScene(ThreeDScene):
return axes
def get_sphere(self, **kwargs):
"""
Returns a sphere with the passed **kwargs
as properties.
Parameters
----------
**kwargs
Some valid kwargs are:
Any param of a Sphere or ParametricSurface.
Returns
-------
Sphere
The sphere object.
"""
config = merge_dicts_recursively(self.sphere_config, kwargs)
return Sphere(**config)
def get_default_camera_position(self):
"""
Returns the default_angled_camera position.
Returns
-------
dict
Dictionary of phi, theta, distance, and gamma.
"""
return self.default_angled_camera_position
def set_camera_to_default_position(self):
"""
Sets the camera to its default position.
"""
self.set_camera_orientation(
**self.default_angled_camera_position
)

View file

@ -47,6 +47,21 @@ class VectorScene(Scene):
}
def add_plane(self, animate=False, **kwargs):
"""
Adds a NumberPlane object to the background.
Parameters
----------
animate : (bool=False)
Whether or not to animate the addition of the plane via ShowCreation.
**kwargs
Any valid keyword arguments accepted by NumberPlane.
Returns
-------
NumberPlane
The NumberPlane object.
"""
plane = NumberPlane(**kwargs)
if animate:
self.play(ShowCreation(plane, lag_ratio=0.5))
@ -54,6 +69,16 @@ class VectorScene(Scene):
return plane
def add_axes(self, animate=False, color=WHITE, **kwargs):
"""
Adds a pair of Axes to the Scene.
Parameters
----------
animate (bool=False)
Whether or not to animate the addition of the axes through ShowCreation.
color (str)
The color of the axes. Defaults to WHITE.
"""
axes = Axes(color=color, tick_frequency=1)
if animate:
self.play(ShowCreation(axes))
@ -61,6 +86,18 @@ class VectorScene(Scene):
return axes
def lock_in_faded_grid(self, dimness=0.7, axes_dimness=0.5):
"""
This method freezes the NumberPlane and Axes that were already
in the background, and adds new, manipulatable ones to the foreground.
Parameters
----------
dimness (Union[int,float=0.7])
The required dimness of the NumberPlane
axes_dimness (Union[int,float=0.5])
The required dimness of the Axes.
"""
plane = self.add_plane()
axes = plane.get_axes()
plane.fade(dimness)
@ -70,6 +107,21 @@ class VectorScene(Scene):
self.freeze_background()
def get_vector(self, numerical_vector, **kwargs):
"""
Returns an arrow on the Plane given an input numerical vector.
Parameters
----------
numerical_vector : Union(np.ndarray, list, tuple)
The Vector to plot.
**kwargs
Any valid keyword argument of Arrow.
Returns
-------
Arrow
The Arrow representing the Vector.
"""
return Arrow(
self.plane.coords_to_point(0, 0),
self.plane.coords_to_point(*numerical_vector[:2]),
@ -78,6 +130,34 @@ class VectorScene(Scene):
)
def add_vector(self, vector, color=YELLOW, animate=True, **kwargs):
"""
Returns the Vector after adding it to the Plane.
Parameters
----------
vector Union(Arrow,list,tuple,np.ndarray)
It can be a pre-made graphical vector, or the
coordinates of one.
color (str)
The string of the hex color of the vector.
This is only taken into consideration if
'vector' is not an Arrow. Defaults to YELLOW.
animate (bool=True)
Whether or not to animate the addition of the vector
by using GrowArrow
**kwargs
Any valid keyword argument of Arrow.
These are only considered if vector is not
an Arrow.
Returns
-------
Arrow
The arrow representing the vector.
"""
if not isinstance(vector, Arrow):
vector = Vector(vector, color=color, **kwargs)
if animate:
@ -86,11 +166,49 @@ class VectorScene(Scene):
return vector
def write_vector_coordinates(self, vector, **kwargs):
"""
Returns a column matrix indicating the vector coordinates,
after writing them to the screen.
Parameters
----------
vector (Arrow)
The arrow representing the vector.
**kwargs
Any valid keyword arguments of matrix.vector_coordinate_label
integer_labels (True) : Whether or not to round the coordinates
to integers.
n_dim (2) : The number of dimensions of the vector.
color (WHITE) : The color of the label.
Returns
-------
Matrix
The column matrix representing the vector.
"""
coords = vector_coordinate_label(vector, **kwargs)
self.play(Write(coords))
return coords
def get_basis_vectors(self, i_hat_color=X_COLOR, j_hat_color=Y_COLOR):
"""
Returns a VGroup of the Basis Vectors (1,0) and (0,1)
Parameters
----------
i_hat_color (str)
The hex colour to use for the basis vector in the x direction
j_hat_color (str)
The hex colour to use for the basis vector in the y direction
Returns
-------
VGroup
VGroup of the Vector Mobjects representing the basis vectors.
"""
return VGroup(*[
Vector(
vect,
@ -104,6 +222,21 @@ class VectorScene(Scene):
])
def get_basis_vector_labels(self, **kwargs):
"""
Returns naming labels for the basis vectors.
Parameters
----------
**kwargs
Any valid keyword arguments of get_vector_label:
vector,
label (str,TexMobject)
at_tip (bool=False),
direction (str="left"),
rotate (bool),
color (str),
label_scale_factor=VECTOR_LABEL_SCALE_FACTOR (int, float),
"""
i_hat, j_hat = self.get_basis_vectors()
return VGroup(*[
self.get_vector_label(
@ -123,6 +256,29 @@ class VectorScene(Scene):
rotate=False,
color=None,
label_scale_factor=VECTOR_LABEL_SCALE_FACTOR):
"""
Returns naming labels for the passed vector.
Parameters
----------
vector
Vector Object for which to get the label.
at_tip (bool)
Whether or not to place the label at the tip of the vector.
direction (str="left")
If the label should be on the "left" or right of the vector.
rotate (bool=False)
Whether or not to rotate it to align it with the vector.
color (str)
The color to give the label.
label_scale_factor (Union[int,float])
How much to scale the label by.
Returns
-------
TexMobject
The TexMobject of the label.
"""
if not isinstance(label, TexMobject):
if len(label) == 1:
label = "\\vec{\\textbf{%s}}" % label
@ -150,23 +306,65 @@ class VectorScene(Scene):
return label
def label_vector(self, vector, label, animate=True, **kwargs):
"""
Shortcut method for creating, and animating the addition of
a label for the vector.
Parameters
----------
vector (Vector)
The vector for which the label must be added.
label (TexMobject,str)
The TexMobject/string of the label.
animate (bool=True)
Whether or not to animate the labelling w/ Write
**kwargs
Any valid keyword argument of get_vector_label
Returns
-------
TexMobject
The TexMobject of the label.
"""
label = self.get_vector_label(vector, label, **kwargs)
if animate:
self.play(Write(label, run_time=1))
self.add(label)
return label
def position_x_coordinate(self, x_coord, x_line, vector):
def position_x_coordinate(self, x_coord, x_line, vector): #TODO Write DocStrings for this.
x_coord.next_to(x_line, -np.sign(vector[1]) * UP)
x_coord.set_color(X_COLOR)
return x_coord
def position_y_coordinate(self, y_coord, y_line, vector):
def position_y_coordinate(self, y_coord, y_line, vector): #TODO Write DocStrings for this.
y_coord.next_to(y_line, np.sign(vector[0]) * RIGHT)
y_coord.set_color(Y_COLOR)
return y_coord
def coords_to_vector(self, vector, coords_start=2 * RIGHT + 2 * UP, clean_up=True):
"""
This method writes the vector as a column matrix (henceforth called the label),
takes the values in it one by one, and form the corresponding
lines that make up the x and y components of the vector. Then, an
Vector() based vector is created between the lines on the Screen.
Parameters
----------
vector Union(np.ndarray, list, tuple)
The vector to show.
coords_start Union(np.ndarray,list,tuple)
The starting point of the location of
the label of the vector that shows it
numerically.
Defaults to 2 * RIGHT + 2 * UP or (2,2)
clean_up (bool=True)
Whether or not to remove whatever
this method did after it's done.
"""
starting_mobjects = list(self.mobjects)
array = Matrix(vector)
array.shift(coords_start)
@ -200,6 +398,26 @@ class VectorScene(Scene):
self.add(*starting_mobjects)
def vector_to_coords(self, vector, integer_labels=True, clean_up=True):
"""
This method displays vector as a Vector() based vector, and then shows
the corresponding lines that make up the x and y components of the vector.
Then, a column matrix (henceforth called the label) is created near the
head of the Vector.
Parameters
----------
vector Union(np.ndarray, list, tuple)
The vector to show.
integer_label (bool=True)
Whether or not to round the value displayed.
in the vector's label to the nearest integer
clean_up (bool=True)
Whether or not to remove whatever
this method did after it's done.
"""
starting_mobjects = list(self.mobjects)
show_creation = False
if isinstance(vector, Arrow):
@ -250,6 +468,17 @@ class VectorScene(Scene):
return array, x_line, y_line
def show_ghost_movement(self, vector):
"""
This method plays an animation that partially shows the entire plane moving
in the direction of a particular vector. This is useful when you wish to
convey the idea of mentally moving the entire plane in a direction, without
actually moving the plane.
Parameters
----------
vector (Union[Arrow, list, tuple, np.ndarray])
The vector which indicates the direction of movement.
"""
if isinstance(vector, Arrow):
vector = vector.get_end() - vector.get_start()
elif len(vector) == 2:
@ -275,6 +504,10 @@ class VectorScene(Scene):
class LinearTransformationScene(VectorScene):
"""
This scene contains special methods that make it
especially suitable for showing Linear Transformations.
"""
CONFIG = {
"include_background_plane": True,
"include_foreground_plane": True,
@ -341,26 +574,85 @@ class LinearTransformationScene(VectorScene):
self.add(self.basis_vectors)
def add_special_mobjects(self, mob_list, *mobs_to_add):
"""
Adds mobjects to a separate list that can be tracked,
if these mobjects have some extra importance.
Parameters
----------
mob_list (list)
The special list to which you want to add
these mobjects.
*mobs_to_add (Mobject)
The mobjects to add.
"""
for mobject in mobs_to_add:
if mobject not in mob_list:
mob_list.append(mobject)
self.add(mobject)
def add_background_mobject(self, *mobjects):
"""
Adds the mobjects to the special list
self.background_mobjects.
Parameters
----------
*mobjects (Mobject)
The mobjects to add to the list.
"""
self.add_special_mobjects(self.background_mobjects, *mobjects)
# TODO, this conflicts with Scene.add_fore
def add_foreground_mobject(self, *mobjects):
"""
Adds the mobjects to the special list
self.foreground_mobjects.
Parameters
----------
*mobjects (Mobject)
The mobjects to add to the list
"""
self.add_special_mobjects(self.foreground_mobjects, *mobjects)
def add_transformable_mobject(self, *mobjects):
"""
Adds the mobjects to the special list
self.transformable_mobjects.
Parameters
----------
*mobjects (Mobject)
The mobjects to add to the list.
"""
self.add_special_mobjects(self.transformable_mobjects, *mobjects)
def add_moving_mobject(self, mobject, target_mobject=None):
"""
Adds the mobject to the special list
self.moving_mobject, and adds a property
to the mobject called mobject.target, which
keeps track of what the mobject will move to
or become etc.
Parameters
----------
mobject (Mobject)
The mobjects to add to the list
target_mobject (Mobject)
What the moving_mobject goes to, etc.
"""
mobject.target = target_mobject
self.add_special_mobjects(self.moving_mobjects, mobject)
def get_unit_square(self, color=YELLOW, opacity=0.3, stroke_width=3):
"""
Returns a unit square for the current NumberPlane.
"""
square = self.square = Rectangle(
color=color,
width=self.plane.get_x_unit_size(),
@ -374,6 +666,24 @@ class LinearTransformationScene(VectorScene):
return square
def add_unit_square(self, animate=False, **kwargs):
"""
Adds a unit square to the scene via
self.get_unit_square.
Parameters
----------
animate (bool)
Whether or not to animate the addition
with DrawBorderThenFill.
**kwargs
Any valid keyword arguments of
self.get_unit_square()
Returns
-------
Square
The unit square.
"""
square = self.get_unit_square(**kwargs)
if animate:
self.play(
@ -386,6 +696,29 @@ class LinearTransformationScene(VectorScene):
return self
def add_vector(self, vector, color=YELLOW, **kwargs):
"""
Adds a vector to the scene, and puts it in the special
list self.moving_vectors.
Parameters
----------
vector Union(Arrow,list,tuple,np.ndarray)
It can be a pre-made graphical vector, or the
coordinates of one.
color (str)
The string of the hex color of the vector.
This is only taken into consideration if
'vector' is not an Arrow. Defaults to YELLOW.
**kwargs
Any valid keyword argument of VectorScene.add_vector.
Returns
-------
Arrow
The arrow representing the vector.
"""
vector = VectorScene.add_vector(
self, vector, color=color, **kwargs
)
@ -393,6 +726,24 @@ class LinearTransformationScene(VectorScene):
return vector
def write_vector_coordinates(self, vector, **kwargs):
"""
Returns a column matrix indicating the vector coordinates,
after writing them to the screen, and adding them to the
special list self.foreground_mobjects
Parameters
----------
vector (Arrow)
The arrow representing the vector.
**kwargs
Any valid keyword arguments of VectorScene.write_vector_coordinates
Returns
-------
Matrix
The column matrix representing the vector.
"""
coords = VectorScene.write_vector_coordinates(self, vector, **kwargs)
self.add_foreground_mobject(coords)
return coords
@ -402,6 +753,26 @@ class LinearTransformationScene(VectorScene):
transformation_name="L",
new_label=None,
**kwargs):
"""
Method for creating, and animating the addition of
a transformable label for the vector.
Parameters
----------
vector (Vector)
The vector for which the label must be added.
label (TexMobject,str)
The TexMobject/string of the label.
new_label (TexMobject,str,None)
What the label should display after a Linear Transformation
**kwargs
Any valid keyword argument of get_vector_label
Returns
-------
TexMobject
The TexMobject of the label.
"""
label_mob = self.label_vector(vector, label, **kwargs)
if new_label:
label_mob.target_text = new_label
@ -418,6 +789,27 @@ class LinearTransformationScene(VectorScene):
return label_mob
def add_title(self, title, scale_factor=1.5, animate=False):
"""
Adds a title, after scaling it, adding a background rectangle,
moving it to the top and adding it to foreground_mobjects adding
it as a local variable of self. Returns the Scene.
Parameters
----------
title (str,TexMobject,TextMobject)
What the title should be.
scale_factor (int,float=1.5)
How much the title should be scaled by.
animate (bool=False)
Whether or not to animate the addition.
Returns
-------
LinearTransformationScene
The scene with the title added to it.
"""
if not isinstance(title, Mobject):
title = TextMobject(title).scale(scale_factor)
title.to_edge(UP)
@ -429,9 +821,28 @@ class LinearTransformationScene(VectorScene):
return self
def get_matrix_transformation(self, matrix):
"""
Returns a function corresponding to the linear
transformation represented by the matrix passed.
Parameters
----------
matrix (np.ndarray, list, tuple)
The matrix.
"""
return self.get_transposed_matrix_transformation(np.array(matrix).T)
def get_transposed_matrix_transformation(self, transposed_matrix):
"""
Returns a function corresponding to the linear
transformation represented by the transposed
matrix passed.
Parameters
----------
matrix (np.ndarray, list, tuple)
The matrix.
"""
transposed_matrix = np.array(transposed_matrix)
if transposed_matrix.shape == (2, 2):
new_matrix = np.identity(3)
@ -442,6 +853,22 @@ class LinearTransformationScene(VectorScene):
return lambda point: np.dot(point, transposed_matrix)
def get_piece_movement(self, pieces):
"""
This method returns an animation that moves an arbitrary
mobject in "pieces" to its corresponding .target value.
If self.leave_ghost_vectors is True, ghosts of the original
positions/mobjects are left on screen
Parameters
----------
pieces (Union[list, tuple, np.array])
The pieces for which the movement must be shown.
Returns
-------
Animation
The animation of the movement.
"""
start = VGroup(*pieces)
target = VGroup(*[mob.target for mob in pieces])
if self.leave_ghost_vectors:
@ -449,6 +876,23 @@ class LinearTransformationScene(VectorScene):
return Transform(start, target, lag_ratio=0)
def get_moving_mobject_movement(self, func):
"""
This method returns an animation that moves a mobject
in "self.moving_mobjects" to its corresponding .target value.
func is a function that determines where the .target goes.
Parameters
----------
func (function)
The function that determines where the .target of
the moving mobject goes.
Returns
-------
Animation
The animation of the movement.
"""
for m in self.moving_mobjects:
if m.target is None:
m.target = m.copy()
@ -457,6 +901,23 @@ class LinearTransformationScene(VectorScene):
return self.get_piece_movement(self.moving_mobjects)
def get_vector_movement(self, func):
"""
This method returns an animation that moves a mobject
in "self.moving_vectors" to its corresponding .target value.
func is a function that determines where the .target goes.
Parameters
----------
func (function)
The function that determines where the .target of
the moving mobject goes.
Returns
-------
Animation
The animation of the movement.
"""
for v in self.moving_vectors:
v.target = Vector(func(v.get_end()), color=v.get_color())
norm = get_norm(v.target.get_end())
@ -465,6 +926,15 @@ class LinearTransformationScene(VectorScene):
return self.get_piece_movement(self.moving_vectors)
def get_transformable_label_movement(self):
"""
This method returns an animation that moves all labels
in "self.transformable_labels" to its corresponding .target .
Returns
-------
Animation
The animation of the movement.
"""
for l in self.transformable_labels:
l.target = self.get_vector_label(
l.vector.target, l.target_text, **l.kwargs
@ -472,12 +942,48 @@ class LinearTransformationScene(VectorScene):
return self.get_piece_movement(self.transformable_labels)
def apply_matrix(self, matrix, **kwargs):
"""
Applies the transformation represented by the
given matrix to the number plane, and each vector/similar
mobject on it.
Parameters
----------
matrix (Union[np.ndarray, list, tuple])
The matrix.
**kwargs
Any valid keyword argument of self.apply_transposed_matrix()
"""
self.apply_transposed_matrix(np.array(matrix).T, **kwargs)
def apply_inverse(self, matrix, **kwargs):
"""
This method applies the linear transformation
represented by the inverse of the passed matrix
to the number plane, and each vector/similar mobject on it.
Parameters
----------
matrix (Union[np.ndarray, list, tuple])
The matrix whose inverse is to be applied.
**kwargs
Any valid keyword argument of self.apply_matrix()
"""
self.apply_matrix(np.linalg.inv(matrix), **kwargs)
def apply_transposed_matrix(self, transposed_matrix, **kwargs):
"""
Applies the transformation represented by the
given transposed matrix to the number plane,
and each vector/similar mobject on it.
Parameters
----------
matrix (Union[np.ndarray, list, tuple])
The matrix.
**kwargs
Any valid keyword argument of self.apply_function()
"""
func = self.get_transposed_matrix_transformation(transposed_matrix)
if "path_arc" not in kwargs:
net_rotation = np.mean([
@ -488,14 +994,56 @@ class LinearTransformationScene(VectorScene):
self.apply_function(func, **kwargs)
def apply_inverse_transpose(self, t_matrix, **kwargs):
"""
Applies the inverse of the transformation represented
by the given transposed matrix to the number plane and each
vector/similar mobject on it.
Parameters
----------
matrix (Union[np.ndarray, list, tuple])
The matrix.
**kwargs
Any valid keyword argument of self.apply_transposed_matrix()
"""
t_inv = np.linalg.inv(np.array(t_matrix).T).T
self.apply_transposed_matrix(t_inv, **kwargs)
def apply_nonlinear_transformation(self, function, **kwargs):
"""
Applies the non-linear transformation represented
by the given function to the number plane and each
vector/similar mobject on it.
Parameters
----------
function (Function)
The function.
**kwargs
Any valid keyword argument of self.apply_function()
"""
self.plane.prepare_for_nonlinear_transform()
self.apply_function(function, **kwargs)
def apply_function(self, function, added_anims=[], **kwargs):
"""
Applies the given function to each of the mobjects in
self.transformable_mobjects, and plays the animation showing
this.
Parameters
----------
function (Function)
The function that affects each point
of each mobject in self.transformable_mobjects.
added_anims (list)
Any other animations that need to be played
simulataneously with this.
**kwargs
Any valid keyword argument of a self.play() call.
"""
if "run_time" not in kwargs:
kwargs["run_time"] = 3
anims = [

View file

@ -11,6 +11,11 @@ from manimlib.utils.simple_functions import fdiv
class ZoomedScene(MovingCameraScene):
"""
This is a Scene with special configurations made for when
a particular part of the scene must be zoomed in on and displayed
separately.
"""
CONFIG = {
"camera_class": MultiCamera,
"zoomed_display_height": 3,
@ -30,6 +35,10 @@ class ZoomedScene(MovingCameraScene):
}
def setup(self):
"""
This method is used internally by Manim to
setup the scene for proper use.
"""
MovingCameraScene.setup(self)
# Initialize camera and display
zoomed_camera = MovingCamera(**self.zoomed_camera_config)
@ -56,6 +65,16 @@ class ZoomedScene(MovingCameraScene):
self.zoomed_display = zoomed_display
def activate_zooming(self, animate=False):
"""
This method is used to activate the zooming for
the zoomed_camera.
Parameters
----------
animate (bool=False)
Whether or not to animate the activation
of the zoomed camera.
"""
self.zoom_activated = True
self.camera.add_image_mobject_from_camera(self.zoomed_display)
if animate:
@ -67,6 +86,21 @@ class ZoomedScene(MovingCameraScene):
)
def get_zoom_in_animation(self, run_time=2, **kwargs):
"""
Returns the animation of camera zooming in.
Parameters
----------
run_time (Union[int,float=2])
The run_time of the animation of the camera zooming in.
**kwargs
Any valid keyword arguments of ApplyMethod()
Returns
-------
ApplyMethod
The animation of the camera zooming in.
"""
frame = self.zoomed_camera.frame
full_frame_height = self.camera.get_frame_height()
full_frame_width = self.camera.get_frame_width()
@ -78,12 +112,32 @@ class ZoomedScene(MovingCameraScene):
return ApplyMethod(frame.restore, run_time=run_time, **kwargs)
def get_zoomed_display_pop_out_animation(self, **kwargs):
"""
This is the animation of the popping out of the
mini-display that shows the content of the zoomed
camera.
Returns
-------
ApplyMethod
The Animation of the Zoomed Display popping out.
"""
display = self.zoomed_display
display.save_state(use_deepcopy=True)
display.replace(self.zoomed_camera.frame, stretch=True)
return ApplyMethod(display.restore)
def get_zoom_factor(self):
"""
Returns the Zoom factor of the Zoomed camera.
Defined as the ratio between the height of the
zoomed camera and the height of the zoomed mini
display.
Returns
-------
float
The zoom factor.
"""
return fdiv(
self.zoomed_camera.frame.get_height(),
self.zoomed_display.get_height()

1
sanim Submodule

@ -0,0 +1 @@
Subproject commit 19aa1e4b2e1c8acff58e075d3d8bc4bd8f112751