This commit is contained in:
鹤翔万里
2021-02-09 09:21:29 +08:00
committed by GitHub
8 changed files with 308 additions and 96 deletions

View File

@ -68,48 +68,6 @@ class OpeningManimExample(Scene):
self.wait(2) self.wait(2)
class InteractiveDevlopment(Scene):
def construct(self):
circle = Circle()
circle.set_fill(BLUE, opacity=0.5)
circle.set_stroke(BLUE_E, width=4)
square = Square()
self.play(ShowCreation(square))
self.wait()
# This opens an iPython termnial where you can keep writing
# lines as if they were part of this construct method
self.embed()
# Try copying and pasting some of the lines below into
# the interactive shell
self.play(ReplacementTransform(square, circle))
self.wait()
self.play(circle.stretch, 4, 0)
self.play(Rotate(circle, 90 * DEGREES))
self.play(circle.shift, 2 * RIGHT, circle.scale, 0.25)
text = Text("""
In general, using the interactive shell
is very helpful when developing new scenes
""")
self.play(Write(text))
# In the interactive shell, you can just type
# play, add, remove, clear, wait, save_state and restore,
# instead of self.play, self.add, self.remove, etc.
# To interact with the window, type touch(). You can then
# scroll in the window, or zoom by holding down 'z' while scrolling,
# and change camera perspective by holding down 'd' while moving
# the mouse. Press 'r' to reset to the standard camera position.
# Press 'q' to stop interacting with the window and go back to
# typing new commands into the shell.
# In principle you can customize a scene
always(circle.move_to, self.mouse_point)
class AnimatingMethods(Scene): class AnimatingMethods(Scene):
def construct(self): def construct(self):
grid = Tex(r"\pi").get_grid(10, 10, height=4) grid = Tex(r"\pi").get_grid(10, 10, height=4)
@ -122,7 +80,10 @@ class AnimatingMethods(Scene):
# to the left, but the following line animates that motion. # to the left, but the following line animates that motion.
self.play(grid.shift, 2 * LEFT) self.play(grid.shift, 2 * LEFT)
# The same applies for any method, including those setting colors. # The same applies for any method, including those setting colors.
self.play(grid.set_color, YELLOW)
self.wait()
self.play(grid.set_submobject_colors_by_gradient, BLUE, GREEN) self.play(grid.set_submobject_colors_by_gradient, BLUE, GREEN)
self.wait()
self.play(grid.set_height, TAU - MED_SMALL_BUFF) self.play(grid.set_height, TAU - MED_SMALL_BUFF)
self.wait() self.wait()
@ -362,6 +323,166 @@ class UpdatersExample(Scene):
self.wait(4 * PI) self.wait(4 * PI)
class GraphExample(Scene):
def construct(self):
axes = Axes((-3, 10), (-1, 8))
axes.add_coordinate_labels()
self.play(Write(axes, lag_ratio=0.01, run_time=1))
# Axes.get_graph will return the graph of a function
sin_graph = axes.get_graph(
lambda x: 2 * math.sin(x),
color=BLUE,
)
# By default, it draws it so as to somewhat smoothly interpolate
# between sampled points (x, f(x)). If the graph is meant to have
# a corner, though, you can set use_smoothing to False
relu_graph = axes.get_graph(
lambda x: max(x, 0),
use_smoothing=False,
color=YELLOW,
)
# For discontinuous functions, you can specify the point of
# discontinuity so that it does not try to draw over the gap.
step_graph = axes.get_graph(
lambda x: 2.0 if x > 3 else 1.0,
discontinuities=[3],
color=GREEN,
)
# Axes.get_graph_label takes in either a string or a mobject.
# If it's a string, it treats it as a LaTeX expression. By default
# it places the label next to the graph near the right side, and
# has it match the color of the graph
sin_label = axes.get_graph_label(sin_graph, "\\sin(x)")
relu_label = axes.get_graph_label(relu_graph, Text("ReLU"))
step_label = axes.get_graph_label(step_graph, Text("Step"), x=4)
self.play(
ShowCreation(sin_graph),
FadeIn(sin_label, RIGHT),
)
self.wait(2)
self.play(
ReplacementTransform(sin_graph, relu_graph),
FadeTransform(sin_label, relu_label),
)
self.wait()
self.play(
ReplacementTransform(relu_graph, step_graph),
FadeTransform(relu_label, step_label),
)
self.wait()
parabola = axes.get_graph(lambda x: 0.25 * x**2)
parabola.set_stroke(BLUE)
self.play(
FadeOut(step_graph),
FadeOut(step_label),
ShowCreation(parabola)
)
self.wait()
# You can use axes.input_to_graph_point, abbreviated
# to axes.i2gp, to find a particular point on a graph
dot = Dot(color=RED)
dot.move_to(axes.i2gp(2, parabola))
self.play(FadeIn(dot, scale=0.5))
# A value tracker lets us animate a parameter, usually
# with the intent of having other mobjects update based
# on the parameter
x_tracker = ValueTracker(2)
f_always(
dot.move_to,
lambda: axes.i2gp(x_tracker.get_value(), parabola)
)
self.play(x_tracker.set_value, 4, run_time=3)
self.play(x_tracker.set_value, -2, run_time=3)
self.wait()
class CoordinateSystemExample(Scene):
def construct(self):
axes = Axes(
# x-axis ranges from -1 to 10, with a default step size of 1
x_range=(-1, 10),
# y-axis ranges from -2 to 10 with a step size of 0.5
y_range=(-2, 2, 0.5),
# The axes will be stretched so as to match the specified
# height and width
height=6,
width=10,
# Axes is made of two NumberLine mobjects. You can specify
# their configuration with axis_config
axis_config={
"stroke_color": GREY_A,
"stroke_width": 2,
},
# Alternatively, you can specify configuration for just one
# of them, like this.
y_axis_config={
"include_tip": False,
}
)
# Keyword arguments of add_coordinate_labels can be used to
# configure the DecimalNumber mobjects which it creates and
# adds to the axes
axes.add_coordinate_labels(
font_size=20,
num_decimal_places=1,
)
self.add(axes)
# Axes descends from the CoordinateSystem class, meaning
# you can call call axes.coords_to_point, abbreviated to
# axes.c2p, to associate a set of coordinates with a point,
# like so:
dot = Dot(color=RED)
dot.move_to(axes.c2p(0, 0))
self.play(FadeIn(dot, scale=0.5))
self.play(dot.move_to, axes.c2p(3, 2))
self.wait()
self.play(dot.move_to, axes.c2p(5, 0.5))
self.wait()
# Similarly, you can call axes.point_to_coords, or axes.p2c
# print(axes.p2c(dot.get_center()))
# We can draw lines from the axes to better mark the coordinates
# of a given point.
# Here, the always_redraw command means that on each new frame
# the lines will be redrawn
h_line = always_redraw(lambda: axes.get_h_line(dot.get_left()))
v_line = always_redraw(lambda: axes.get_v_line(dot.get_bottom()))
self.play(
ShowCreation(h_line),
ShowCreation(v_line),
)
self.play(dot.move_to, axes.c2p(3, -2))
self.wait()
self.play(dot.move_to, axes.c2p(1, 1))
self.wait()
# If we tie the dot to a particular set of coordinates, notice
# that as we move the axes around it respects the coordinate
# system defined by them.
f_always(dot.move_to, lambda: axes.c2p(1, 1))
self.play(
axes.scale, 0.75,
axes.to_corner, UL,
run_time=2,
)
self.wait()
self.play(FadeOut(VGroup(axes, dot, h_line, v_line)))
# Other coordinate systems you can play around with include
# ThreeDAxes, NumberPlane, and ComplexPlane.
class SurfaceExample(Scene): class SurfaceExample(Scene):
CONFIG = { CONFIG = {
"camera_class": ThreeDCamera, "camera_class": ThreeDCamera,
@ -453,6 +574,52 @@ class SurfaceExample(Scene):
self.wait() self.wait()
class InteractiveDevlopment(Scene):
def construct(self):
circle = Circle()
circle.set_fill(BLUE, opacity=0.5)
circle.set_stroke(BLUE_E, width=4)
square = Square()
self.play(ShowCreation(square))
self.wait()
# This opens an iPython termnial where you can keep writing
# lines as if they were part of this construct method.
# In particular, 'square', 'circle' and 'self' will all be
# part of the local namespace in that terminal.
self.embed()
# Try copying and pasting some of the lines below into
# the interactive shell
self.play(ReplacementTransform(square, circle))
self.wait()
self.play(circle.stretch, 4, 0)
self.play(Rotate(circle, 90 * DEGREES))
self.play(circle.shift, 2 * RIGHT, circle.scale, 0.25)
text = Text("""
In general, using the interactive shell
is very helpful when developing new scenes
""")
self.play(Write(text))
# In the interactive shell, you can just type
# play, add, remove, clear, wait, save_state and restore,
# instead of self.play, self.add, self.remove, etc.
# To interact with the window, type touch(). You can then
# scroll in the window, or zoom by holding down 'z' while scrolling,
# and change camera perspective by holding down 'd' while moving
# the mouse. Press 'r' to reset to the standard camera position.
# Press 'q' to stop interacting with the window and go back to
# typing new commands into the shell.
# In principle you can customize a scene to be responsive to
# mouse and keyboard interactions
always(circle.move_to, self.mouse_point)
class ControlsExample(Scene): class ControlsExample(Scene):
def setup(self): def setup(self):
self.textbox = Textbox() self.textbox = Textbox()

View File

@ -5,6 +5,7 @@ from manimlib.constants import *
from manimlib.mobject.functions import ParametricCurve from manimlib.mobject.functions import ParametricCurve
from manimlib.mobject.geometry import Arrow from manimlib.mobject.geometry import Arrow
from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import DashedLine
from manimlib.mobject.geometry import Rectangle from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.number_line import NumberLine from manimlib.mobject.number_line import NumberLine
from manimlib.mobject.svg.tex_mobject import Tex from manimlib.mobject.svg.tex_mobject import Tex
@ -24,8 +25,8 @@ class CoordinateSystem():
""" """
CONFIG = { CONFIG = {
"dimension": 2, "dimension": 2,
"x_range": [-8, 8, 1], "x_range": np.array([-8, 8, 1]),
"y_range": [-4, 4, 1], "y_range": np.array([-4, 4, 1]),
"width": None, "width": None,
"height": None, "height": None,
"num_sampled_graph_points_per_tick": 5, "num_sampled_graph_points_per_tick": 5,
@ -88,12 +89,26 @@ class CoordinateSystem():
) )
return self.axis_labels return self.axis_labels
def get_line_from_axis_to_point(self, index, point,
line_func=DashedLine,
color=GREY_A,
stroke_width=2):
axis = self.get_axis(index)
line = line_func(axis.get_projection(point), point)
line.set_stroke(color, stroke_width)
return line
def get_v_line(self, point, **kwargs):
return self.get_line_from_axis_to_point(0, point, **kwargs)
def get_h_line(self, point, **kwargs):
return self.get_line_from_axis_to_point(1, point, **kwargs)
# Useful for graphing # Useful for graphing
def get_graph(self, function, x_range=None, **kwargs): def get_graph(self, function, x_range=None, **kwargs):
t_range = list(self.x_range) t_range = np.array(self.x_range, dtype=float)
if x_range is not None: if x_range is not None:
for i in range(len(x_range)): t_range[:len(x_range)] = x_range
t_range[i] = x_range[i]
# For axes, the third coordinate of x_range indicates # For axes, the third coordinate of x_range indicates
# tick frequency. But for functions, it indicates a # tick frequency. But for functions, it indicates a
# sample frequency # sample frequency
@ -134,7 +149,7 @@ class CoordinateSystem():
else: else:
return None return None
def itgp(self, x, graph): def i2gp(self, x, graph):
""" """
Alias for input_to_graph_point Alias for input_to_graph_point
""" """
@ -147,6 +162,7 @@ class CoordinateSystem():
direction=RIGHT, direction=RIGHT,
buff=MED_SMALL_BUFF, buff=MED_SMALL_BUFF,
color=None): color=None):
if isinstance(label, str):
label = Tex(label) label = Tex(label)
if color is None: if color is None:
label.match_color(graph) label.match_color(graph)
@ -154,8 +170,10 @@ class CoordinateSystem():
# Searching from the right, find a point # Searching from the right, find a point
# whose y value is in bounds # whose y value is in bounds
max_y = FRAME_Y_RADIUS - label.get_height() max_y = FRAME_Y_RADIUS - label.get_height()
for x0 in np.arange(*self.x_range)[-1::-1]: max_x = FRAME_X_RADIUS - label.get_width()
if abs(self.itgp(x0, graph)[1]) < max_y: for x0 in np.arange(*self.x_range)[::-1]:
pt = self.i2gp(x0, graph)
if abs(pt[0]) < max_x and abs(pt[1]) < max_y:
x = x0 x = x0
break break
if x is None: if x is None:
@ -170,11 +188,11 @@ class CoordinateSystem():
label.shift_onto_screen() label.shift_onto_screen()
return label return label
def get_vertical_line_to_graph(self, x, graph, line_func=Line): def get_v_line_to_graph(self, x, graph, **kwargs):
return line_func( return self.get_v_line(self.i2gp(x, graph), **kwargs)
self.coords_to_point(x, 0),
self.input_to_graph_point(x, graph), def get_h_line_to_graph(self, x, graph, **kwargs):
) return self.get_h_line(self.i2gp(x, graph), **kwargs)
# For calculus # For calculus
def angle_of_tangent(self, x, graph, dx=EPSILON): def angle_of_tangent(self, x, graph, dx=EPSILON):
@ -221,7 +239,7 @@ class CoordinateSystem():
else: else:
raise Exception("Invalid input sample type") raise Exception("Invalid input sample type")
height = get_norm( height = get_norm(
self.itgp(sample, graph) - self.c2p(sample, 0) self.i2gp(sample, graph) - self.c2p(sample, 0)
) )
rect = Rectangle(width=x1 - x0, height=height) rect = Rectangle(width=x1 - x0, height=height)
rect.move_to(self.c2p(x0, 0), DL) rect.move_to(self.c2p(x0, 0), DL)
@ -244,21 +262,25 @@ class Axes(VGroup, CoordinateSystem):
CONFIG = { CONFIG = {
"axis_config": { "axis_config": {
"include_tip": True, "include_tip": True,
"numbers_to_exclude": [0],
}, },
"x_axis_config": {}, "x_axis_config": {},
"y_axis_config": { "y_axis_config": {
"line_to_number_direction": LEFT, "line_to_number_direction": LEFT,
}, },
"height": FRAME_HEIGHT - 2,
"width": FRAME_WIDTH - 2,
} }
def __init__(self, x_range=None, y_range=None, **kwargs): def __init__(self,
VGroup.__init__(self, **kwargs) x_range=None,
y_range=None,
**kwargs):
super().__init__(**kwargs)
if x_range is not None: if x_range is not None:
for i in range(len(x_range)): self.x_range[:len(x_range)] = x_range
self.x_range[i] = x_range[i]
if y_range is not None: if y_range is not None:
for i in range(len(y_range)): self.y_range[:len(x_range)] = y_range
self.y_range[i] = y_range[i]
self.x_axis = self.create_axis( self.x_axis = self.create_axis(
self.x_range, self.x_axis_config, self.width, self.x_range, self.x_axis_config, self.width,
@ -300,24 +322,21 @@ class Axes(VGroup, CoordinateSystem):
def add_coordinate_labels(self, def add_coordinate_labels(self,
x_values=None, x_values=None,
y_values=None, y_values=None,
excluding=[0],
**kwargs): **kwargs):
axes = self.get_axes() axes = self.get_axes()
self.coordinate_labels = VGroup() self.coordinate_labels = VGroup()
for axis, values in zip(axes, [x_values, y_values]): for axis, values in zip(axes, [x_values, y_values]):
numbers = axis.add_numbers( labels = axis.add_numbers(values, **kwargs)
values, excluding=excluding, **kwargs self.coordinate_labels.add(labels)
)
self.coordinate_labels.add(numbers)
return self.coordinate_labels return self.coordinate_labels
class ThreeDAxes(Axes): class ThreeDAxes(Axes):
CONFIG = { CONFIG = {
"dimension": 3, "dimension": 3,
"x_range": (-6, 6, 1), "x_range": np.array([-6, 6, 1]),
"y_range": (-5, 5, 1), "y_range": np.array([-5, 5, 1]),
"z_range": (-4, 4, 1), "z_range": np.array([-4, 4, 1]),
"z_axis_config": {}, "z_axis_config": {},
"z_normal": DOWN, "z_normal": DOWN,
"depth": None, "depth": None,
@ -365,6 +384,8 @@ class NumberPlane(Axes):
"stroke_width": 2, "stroke_width": 2,
"stroke_opacity": 1, "stroke_opacity": 1,
}, },
"height": None,
"width": None,
# Defaults to a faded version of line_config # Defaults to a faded version of line_config
"faded_line_style": None, "faded_line_style": None,
"faded_line_ratio": 1, "faded_line_ratio": 1,

View File

@ -9,6 +9,7 @@ class ParametricCurve(VMobject):
"epsilon": 1e-8, "epsilon": 1e-8,
# TODO, automatically figure out discontinuities # TODO, automatically figure out discontinuities
"discontinuities": [], "discontinuities": [],
"use_smoothing": True,
} }
def __init__(self, t_func, t_range=None, **kwargs): def __init__(self, t_func, t_range=None, **kwargs):
@ -39,6 +40,7 @@ class ParametricCurve(VMobject):
points = np.array([self.t_func(t) for t in t_range]) points = np.array([self.t_func(t) for t in t_range])
self.start_new_path(points[0]) self.start_new_path(points[0])
self.add_points_as_corners(points[1:]) self.add_points_as_corners(points[1:])
if self.use_smoothing:
self.make_approximately_smooth() self.make_approximately_smooth()
return self return self

View File

@ -472,6 +472,14 @@ class Line(TipableVMobject):
def get_angle(self): def get_angle(self):
return angle_of_vector(self.get_vector()) return angle_of_vector(self.get_vector())
def get_projection(self, point):
"""
Return projection of a point onto the line
"""
unit_vect = self.get_unit_vector()
start = self.get_start()
return start + np.dot(point - start, unit_vect) * unit_vect
def get_slope(self): def get_slope(self):
return np.tan(self.get_angle()) return np.tan(self.get_angle())

View File

@ -149,17 +149,22 @@ class NumberLine(Line):
num_mob.shift(num_mob[0].get_width() * LEFT / 2) num_mob.shift(num_mob[0].get_width() * LEFT / 2)
return num_mob return num_mob
def add_numbers(self, x_values=None, excluding=None, **kwargs): def add_numbers(self, x_values=None, excluding=None, font_size=24, **kwargs):
if x_values is None: if x_values is None:
x_values = self.get_tick_range() x_values = self.get_tick_range()
if excluding is not None:
x_values = list_difference_update(x_values, excluding)
self.numbers = VGroup() kwargs["font_size"] = font_size
numbers = VGroup()
for x in x_values: for x in x_values:
self.numbers.add(self.get_number_mobject(x, **kwargs)) if x in self.numbers_to_exclude:
self.add(self.numbers) continue
return self.numbers if excluding is not None and x in excluding:
continue
numbers.add(self.get_number_mobject(x, **kwargs))
self.add(numbers)
self.numbers = numbers
return numbers
class UnitInterval(NumberLine): class UnitInterval(NumberLine):

View File

@ -156,10 +156,6 @@ class Square3D(Surface):
class Cube(SGroup): class Cube(SGroup):
CONFIG = { CONFIG = {
# "fill_color": BLUE,
# "fill_opacity": 1,
# "stroke_width": 1,
# "stroke_color": BLACK,
"color": BLUE, "color": BLUE,
"opacity": 1, "opacity": 1,
"gloss": 0.5, "gloss": 0.5,
@ -174,7 +170,6 @@ class Cube(SGroup):
face.apply_matrix(z_to_vector(vect)) face.apply_matrix(z_to_vector(vect))
self.add(face) self.add(face)
self.set_height(self.side_length) self.set_height(self.side_length)
# self.set_color(self.color, self.opacity, self.gloss)
class Prism(Cube): class Prism(Cube):

View File

@ -101,37 +101,51 @@ class Surface(Mobject):
return normalize_along_axis(normals, 1) return normalize_along_axis(normals, 1)
def pointwise_become_partial(self, smobject, a, b, axis=None): def pointwise_become_partial(self, smobject, a, b, axis=None):
assert(isinstance(smobject, Surface))
if axis is None: if axis is None:
axis = self.prefered_creation_axis axis = self.prefered_creation_axis
assert(isinstance(smobject, Surface))
if a <= 0 and b >= 1: if a <= 0 and b >= 1:
self.match_points(smobject) self.match_points(smobject)
return self return self
nu, nv = smobject.resolution nu, nv = smobject.resolution
self.set_points(np.vstack([ self.set_points(np.vstack([
self.get_partial_points_array(arr, a, b, (nu, nv, 3), axis=axis) self.get_partial_points_array(arr.copy(), a, b, (nu, nv, 3), axis=axis)
for arr in smobject.get_surface_points_and_nudged_points() for arr in smobject.get_surface_points_and_nudged_points()
])) ]))
return self return self
def get_partial_points_array(self, points, a, b, resolution, axis): def get_partial_points_array(self, points, a, b, resolution, axis):
if len(points) == 0:
return points
nu, nv = resolution[:2] nu, nv = resolution[:2]
points = points.reshape(resolution) points = points.reshape(resolution)
max_index = resolution[axis] - 1 max_index = resolution[axis] - 1
lower_index, lower_residue = integer_interpolate(0, max_index, a) lower_index, lower_residue = integer_interpolate(0, max_index, a)
upper_index, upper_residue = integer_interpolate(0, max_index, b) upper_index, upper_residue = integer_interpolate(0, max_index, b)
if axis == 0: if axis == 0:
points[:lower_index] = interpolate(points[lower_index], points[lower_index + 1], lower_residue) points[:lower_index] = interpolate(
points[upper_index:] = interpolate(points[upper_index], points[upper_index + 1], upper_residue) points[lower_index],
points[lower_index + 1],
lower_residue
)
points[upper_index + 1:] = interpolate(
points[upper_index],
points[upper_index + 1],
upper_residue
)
else: else:
tuples = [ shape = (nu, 1, resolution[2])
(points[:, :lower_index], lower_index, lower_residue), points[:, :lower_index] = interpolate(
(points[:, upper_index:], upper_index, upper_residue), points[:, lower_index],
] points[:, lower_index + 1],
for to_change, index, residue in tuples: lower_residue
col = interpolate(points[:, index], points[:, index + 1], residue) ).reshape(shape)
to_change[:] = col.reshape((nu, 1, *resolution[2:])) points[:, upper_index + 1:] = interpolate(
points[:, upper_index],
points[:, upper_index + 1],
upper_residue
).reshape(shape)
return points.reshape((nu * nv, *resolution[2:])) return points.reshape((nu * nv, *resolution[2:]))
def sort_faces_back_to_front(self, vect=OUT): def sort_faces_back_to_front(self, vect=OUT):

View File

@ -56,7 +56,7 @@ mat4 get_xyz_to_uv(vec3 b0, vec3 b1, vec3 unit_normal){
// float get_reduced_control_points(vec3 b0, vec3 b1, vec3 b2, out vec3 new_points[3]){ // float get_reduced_control_points(vec3 b0, vec3 b1, vec3 b2, out vec3 new_points[3]){
float get_reduced_control_points(in vec3 points[3], out vec3 new_points[3]){ float get_reduced_control_points(in vec3 points[3], out vec3 new_points[3]){
float length_threshold = 1e-6; float length_threshold = 1e-6;
float angle_threshold = 1e-3; float angle_threshold = 5e-2;
vec3 p0 = points[0]; vec3 p0 = points[0];
vec3 p1 = points[1]; vec3 p1 = points[1];