diff --git a/example_scenes.py b/example_scenes.py index feff9bd7..60892d6c 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -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() diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index e03ea6d0..980b5106 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -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,15 +162,18 @@ class CoordinateSystem(): direction=RIGHT, buff=MED_SMALL_BUFF, color=None): - label = Tex(label) + if isinstance(label, str): + label = Tex(label) if color is None: label.match_color(graph) if x is None: # 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, diff --git a/manimlib/mobject/functions.py b/manimlib/mobject/functions.py index 26a492a5..d0d64cee 100644 --- a/manimlib/mobject/functions.py +++ b/manimlib/mobject/functions.py @@ -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,7 +40,8 @@ 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:]) - self.make_approximately_smooth() + if self.use_smoothing: + self.make_approximately_smooth() return self diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 6eebf6b4..9ed5cefb 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -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()) diff --git a/manimlib/mobject/number_line.py b/manimlib/mobject/number_line.py index a8fefb01..e51e9f8f 100644 --- a/manimlib/mobject/number_line.py +++ b/manimlib/mobject/number_line.py @@ -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): diff --git a/manimlib/mobject/three_dimensions.py b/manimlib/mobject/three_dimensions.py index 08024296..75740ae7 100644 --- a/manimlib/mobject/three_dimensions.py +++ b/manimlib/mobject/three_dimensions.py @@ -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): diff --git a/manimlib/mobject/types/surface.py b/manimlib/mobject/types/surface.py index 1008fe63..b5e1571e 100644 --- a/manimlib/mobject/types/surface.py +++ b/manimlib/mobject/types/surface.py @@ -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): diff --git a/manimlib/shaders/inserts/quadratic_bezier_geometry_functions.glsl b/manimlib/shaders/inserts/quadratic_bezier_geometry_functions.glsl index c0f77bb3..e27402d1 100644 --- a/manimlib/shaders/inserts/quadratic_bezier_geometry_functions.glsl +++ b/manimlib/shaders/inserts/quadratic_bezier_geometry_functions.glsl @@ -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];