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)
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):
def construct(self):
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.
self.play(grid.shift, 2 * LEFT)
# 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.wait()
self.play(grid.set_height, TAU - MED_SMALL_BUFF)
self.wait()
@ -362,6 +323,166 @@ class UpdatersExample(Scene):
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):
CONFIG = {
"camera_class": ThreeDCamera,
@ -453,6 +574,52 @@ class SurfaceExample(Scene):
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):
def setup(self):
self.textbox = Textbox()

View File

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

View File

@ -9,6 +9,7 @@ class ParametricCurve(VMobject):
"epsilon": 1e-8,
# TODO, automatically figure out discontinuities
"discontinuities": [],
"use_smoothing": True,
}
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])
self.start_new_path(points[0])
self.add_points_as_corners(points[1:])
if self.use_smoothing:
self.make_approximately_smooth()
return self

View File

@ -472,6 +472,14 @@ class Line(TipableVMobject):
def get_angle(self):
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):
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)
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:
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:
self.numbers.add(self.get_number_mobject(x, **kwargs))
self.add(self.numbers)
return self.numbers
if x in self.numbers_to_exclude:
continue
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):

View File

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

View File

@ -101,37 +101,51 @@ class Surface(Mobject):
return normalize_along_axis(normals, 1)
def pointwise_become_partial(self, smobject, a, b, axis=None):
assert(isinstance(smobject, Surface))
if axis is None:
axis = self.prefered_creation_axis
assert(isinstance(smobject, Surface))
if a <= 0 and b >= 1:
self.match_points(smobject)
return self
nu, nv = smobject.resolution
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()
]))
return self
def get_partial_points_array(self, points, a, b, resolution, axis):
if len(points) == 0:
return points
nu, nv = resolution[:2]
points = points.reshape(resolution)
max_index = resolution[axis] - 1
lower_index, lower_residue = integer_interpolate(0, max_index, a)
upper_index, upper_residue = integer_interpolate(0, max_index, b)
if axis == 0:
points[:lower_index] = interpolate(points[lower_index], points[lower_index + 1], lower_residue)
points[upper_index:] = interpolate(points[upper_index], points[upper_index + 1], upper_residue)
points[:lower_index] = interpolate(
points[lower_index],
points[lower_index + 1],
lower_residue
)
points[upper_index + 1:] = interpolate(
points[upper_index],
points[upper_index + 1],
upper_residue
)
else:
tuples = [
(points[:, :lower_index], lower_index, lower_residue),
(points[:, upper_index:], upper_index, upper_residue),
]
for to_change, index, residue in tuples:
col = interpolate(points[:, index], points[:, index + 1], residue)
to_change[:] = col.reshape((nu, 1, *resolution[2:]))
shape = (nu, 1, resolution[2])
points[:, :lower_index] = interpolate(
points[:, lower_index],
points[:, lower_index + 1],
lower_residue
).reshape(shape)
points[:, upper_index + 1:] = interpolate(
points[:, upper_index],
points[:, upper_index + 1],
upper_residue
).reshape(shape)
return points.reshape((nu * nv, *resolution[2:]))
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(in vec3 points[3], out vec3 new_points[3]){
float length_threshold = 1e-6;
float angle_threshold = 1e-3;
float angle_threshold = 5e-2;
vec3 p0 = points[0];
vec3 p1 = points[1];