diff --git a/manimlib/imports.py b/manimlib/imports.py index 6a2035dd..f3f4709b 100644 --- a/manimlib/imports.py +++ b/manimlib/imports.py @@ -67,11 +67,9 @@ from manimlib.once_useful_constructs.fractals import * from manimlib.once_useful_constructs.graph_theory import * from manimlib.once_useful_constructs.light import * -from manimlib.scene.graph_scene import * from manimlib.scene.reconfigurable_scene import * from manimlib.scene.scene import * from manimlib.scene.sample_space_scene import * -from manimlib.scene.graph_scene import * from manimlib.scene.three_d_scene import * from manimlib.scene.vector_space_scene import * diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index bb7d6a0e..04601289 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -5,14 +5,17 @@ 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 Rectangle from manimlib.mobject.number_line import NumberLine from manimlib.mobject.svg.tex_mobject import Tex from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.utils.config_ops import merge_dicts_recursively from manimlib.utils.simple_functions import binary_search from manimlib.utils.space_ops import angle_of_vector +from manimlib.utils.space_ops import get_norm +from manimlib.utils.space_ops import rotate_vector -# TODO: There should be much more code reuse between Axes, NumberPlane and GraphScene +EPSILON = 1e-8 class CoordinateSystem(): @@ -25,6 +28,7 @@ class CoordinateSystem(): "y_range": [-4, 4, 1], "width": None, "height": None, + "num_sampled_graph_points_per_tick": 5, } def coords_to_point(self, *coords): @@ -84,12 +88,21 @@ class CoordinateSystem(): ) return self.axis_labels + # Useful for graphing def get_graph(self, function, x_range=None, **kwargs): - if x_range is None: - x_range = self.x_range + t_range = list(self.x_range) + if x_range is not None: + for i in range(len(x_range)): + t_range[i] = x_range[i] + # For axes, the third coordinate of x_range indicates + # tick frequency. But for functions, it indicates a + # sample frequency + if x_range is None or len(x_range) < 3: + t_range[2] /= self.num_sampled_graph_points_per_tick + graph = ParametricCurve( - lambda t: self.coords_to_point(t, function(t)), - t_range=x_range, + lambda t: self.c2p(t, function(t)), + t_range=t_range, **kwargs ) graph.underlying_function = function @@ -121,6 +134,111 @@ class CoordinateSystem(): else: return None + def itgp(self, x, graph): + """ + Alias for input_to_graph_point + """ + return self.input_to_graph_point(x, graph) + + def get_graph_label(self, + graph, + label="f(x)", + x=None, + direction=RIGHT, + buff=MED_SMALL_BUFF, + color=None): + 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: + x = x0 + break + if x is None: + x = self.x_range[1] + + point = self.input_to_graph_point(x, graph) + angle = self.angle_of_tangent(x, graph) + normal = rotate_vector(RIGHT, angle + 90 * DEGREES) + if normal[1] < 0: + normal *= -1 + label.next_to(point, normal, buff=buff) + 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), + ) + + # For calculus + def angle_of_tangent(self, x, graph, dx=EPSILON): + p0 = self.input_to_graph_point(x, graph) + p1 = self.input_to_graph_point(x + dx, graph) + return angle_of_vector(p1 - p0) + + def slope_of_tangent(self, x, graph, **kwargs): + return np.tan(self.angle_of_tangent(x, graph, **kwargs)) + + def get_tangent_line(self, x, graph, length=5, line_func=Line): + line = line_func(LEFT, RIGHT) + line.set_width(length) + line.rotate(self.angle_of_tangent(x, graph)) + line.move_to(self.input_to_graph_point(x, graph)) + return line + + def get_riemann_rectangles(self, + graph, + x_range=None, + dx=None, + input_sample_type="left", + stroke_width=1, + stroke_color=BLACK, + fill_opacity=1, + colors=(BLUE, GREEN), + show_signed_area=True): + if x_range is None: + x_range = self.x_range[:2] + if dx is None: + dx = self.x_range[2] + if len(x_range) < 3: + x_range = [*x_range, dx] + + rects = [] + xs = np.arange(*x_range) + for x0, x1 in zip(xs, xs[1:]): + if input_sample_type == "left": + sample = x0 + elif input_sample_type == "right": + sample = x1 + elif input_sample_type == "center": + sample = 0.5 * x0 + 0.5 * x1 + else: + raise Exception("Invalid input sample type") + height = get_norm( + self.itgp(sample, graph) - self.c2p(sample, 0) + ) + rect = Rectangle(width=x1 - x0, height=height) + rect.move_to(self.c2p(x0, 0), DL) + rects.append(rect) + result = VGroup(*rects) + result.set_submobject_colors_by_gradient(*colors) + result.set_style( + stroke_width=stroke_width, + stroke_color=stroke_color, + fill_opacity=fill_opacity, + ) + return result + + def get_area_under_graph(self, graph, x_range, fill_color=BLUE, fill_opacity=1): + # TODO + pass + class Axes(VGroup, CoordinateSystem): CONFIG = { @@ -135,15 +253,18 @@ class Axes(VGroup, CoordinateSystem): def __init__(self, x_range=None, y_range=None, **kwargs): VGroup.__init__(self, **kwargs) + if x_range is not None: + for i in range(len(x_range)): + self.x_range[i] = x_range[i] + if y_range is not None: + for i in range(len(y_range)): + self.y_range[i] = y_range[i] + self.x_axis = self.create_axis( - x_range or self.x_range, - self.x_axis_config, - self.width, + self.x_range, self.x_axis_config, self.width, ) self.y_axis = self.create_axis( - y_range or self.y_range, - self.y_axis_config, - self.height + self.y_range, self.y_axis_config, self.height ) self.y_axis.rotate(90 * DEGREES, about_point=ORIGIN) # Add as a separate group in case various other @@ -162,7 +283,7 @@ class Axes(VGroup, CoordinateSystem): def coords_to_point(self, *coords): origin = self.x_axis.number_to_point(0) - result = np.array(origin) + result = origin.copy() for axis, coord in zip(self.get_axes(), coords): result += (axis.number_to_point(coord) - origin) return result diff --git a/manimlib/scene/graph_scene.py b/manimlib/once_useful_constructs/graph_scene.py similarity index 98% rename from manimlib/scene/graph_scene.py rename to manimlib/once_useful_constructs/graph_scene.py index cc3faf78..40d7e790 100644 --- a/manimlib/scene/graph_scene.py +++ b/manimlib/once_useful_constructs/graph_scene.py @@ -19,10 +19,9 @@ from manimlib.utils.color import color_gradient from manimlib.utils.color import invert_color from manimlib.utils.space_ops import angle_of_vector -# TODO, this should probably reimplemented entirely, especially so as to -# better reuse code from mobject/coordinate_systems. -# Also, I really dislike how the configuration is set up, this -# is way too messy to work with. +# TODO, this class should be deprecated, with all its +# functionality moved to Axes and handled at the mobject +# level rather than the scene level class GraphScene(Scene):