diff --git a/active_projects/holomorphic.py b/active_projects/holomorphic.py index 13516630..e4b17922 100644 --- a/active_projects/holomorphic.py +++ b/active_projects/holomorphic.py @@ -41,7 +41,7 @@ class AnalyzeZSquared(ComplexTransformationScene, ZoomedScene): self.add_foreground_mobject(title) def edit_background_plane(self): - self.background.main_lines.set_stroke(GREY, 2) + self.backgrounds.set_stroke(GREY, 2) self.background.secondary_lines.set_stroke(DARK_GREY, 1) self.add_foreground_mobject(self.background.coordinate_labels) diff --git a/manimlib/container/container.py b/manimlib/container/container.py index d84ff2a0..5686673f 100644 --- a/manimlib/container/container.py +++ b/manimlib/container/container.py @@ -14,7 +14,7 @@ from manimlib.utils.config_ops import digest_config class Container(object): - def __init__(self, *submobjects, **kwargs): + def __init__(self, **kwargs): digest_config(self, kwargs) def add(self, *items): diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index 9e2b6191..eb325c1e 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -1,4 +1,5 @@ import numpy as np +import itertools as it from manimlib.constants import * from manimlib.mobject.functions import ParametricFunction @@ -7,17 +8,90 @@ from manimlib.mobject.geometry import Line from manimlib.mobject.number_line import NumberLine from manimlib.mobject.svg.tex_mobject import TexMobject from manimlib.mobject.types.vectorized_mobject import VGroup -from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.config_ops import digest_config from manimlib.utils.config_ops import merge_config +from manimlib.utils.simple_functions import binary_search from manimlib.utils.space_ops import angle_of_vector # TODO: There should be much more code reuse between Axes, NumberPlane and GraphScene -class Axes(VGroup): +class CoordinateSystem(): + """ + Abstract class for Axes and NumberPlane + """ + CONFIG = { + "dimension": 2, + "x_min": -FRAME_X_RADIUS, + "x_max": FRAME_X_RADIUS, + "y_min": -FRAME_Y_RADIUS, + "y_max": FRAME_Y_RADIUS, + } + + def coords_to_point(self, *coords): + raise Exception("Not implemented") + + def point_to_coords(self, point): + raise Exception("Not implemented") + + def get_axes(self): + raise Exception("Not implemented") + + def get_axis(self, index): + return self.get_axes()[index] + + def get_x_axis(self): + return self.get_axis(0) + + def get_y_axis(self): + return self.get_axis(1) + + def get_z_axis(self): + return self.get_axis(2) + + def get_graph(self, function, **kwargs): + x_min = kwargs.pop("x_min", self.x_min) + x_max = kwargs.pop("x_max", self.x_max) + graph = ParametricFunction( + lambda t: self.coords_to_point(t, function(t)), + t_min=x_min, + t_max=x_max, + **kwargs + ) + graph.underlying_function = function + return graph + + def get_parametric_curve(self, function, **kwargs): + dim = self.dimension + graph = ParametricFunction( + lambda t: self.coords_to_point( + *function(t)[:dim] + ), + **kwargs + ) + graph.underlying_function = function + return graph + + def input_to_graph_point(self, x, graph): + if hasattr(graph, "underlying_function"): + return self.coords_to_point(x, graph.underlying_function(x)) + else: + alpha = binary_search( + function=lambda a: self.point_to_coords( + graph.point_from_proportion(a) + )[0], + target=x, + lower_bound=self.x_min, + uplper_bound=self.x_max, + ) + if alpha is not None: + return graph.point_from_proportion(alpha) + else: + return None + + +class Axes(VGroup, CoordinateSystem): CONFIG = { - "three_d": False, "number_line_config": { "color": LIGHT_GREY, "include_tip": True, @@ -26,24 +100,26 @@ class Axes(VGroup): "y_axis_config": { "label_direction": LEFT, }, - "x_min": -FRAME_X_RADIUS, - "x_max": FRAME_X_RADIUS, - "y_min": -FRAME_Y_RADIUS, - "y_max": FRAME_Y_RADIUS, + "center_point": ORIGIN, } def __init__(self, **kwargs): VGroup.__init__(self, **kwargs) - self.x_axis = self.get_axis( + self.x_axis = self.create_axis( self.x_min, self.x_max, self.x_axis_config ) - self.y_axis = self.get_axis( + self.y_axis = self.create_axis( self.y_min, self.y_max, self.y_axis_config ) self.y_axis.rotate(90 * DEGREES, about_point=ORIGIN) - self.add(self.x_axis, self.y_axis) + # Add as a separate group incase various other + # mobjects are added to self, as for example in + # NumberPlane below + self.axes = VGroup(self.x_axis, self.y_axis) + self.add(*self.axes) + self.shift(self.center_point) - def get_axis(self, min_val, max_val, axis_config): + def create_axis(self, min_val, max_val, axis_config): new_config = merge_config([ axis_config, {"x_min": min_val, "x_max": max_val}, @@ -54,63 +130,23 @@ class Axes(VGroup): def coords_to_point(self, *coords): origin = self.x_axis.number_to_point(0) result = np.array(origin) - for axis, coord in zip(self, coords): + for axis, coord in zip(self.get_axes(), coords): result += (axis.number_to_point(coord) - origin) return result def point_to_coords(self, point): return tuple([ axis.point_to_number(point) - for axis in self - if isinstance(axis, NumberLine) + for axis in self.get_axes() ]) - def get_graph( - self, function, - x_min=None, - x_max=None, - **kwargs - ): - kwargs["fill_opacity"] = kwargs.get("fill_opacity", 0) - x_min = x_min or self.x_min - x_max = x_max or self.x_max - graph = ParametricFunction( - lambda t: self.coords_to_point(t, function(t)), - t_min=x_min, - t_max=x_max, - **kwargs - ) - graph.underlying_function = function - return graph - - def input_to_graph_point(self, x, graph): - if hasattr(graph, "underlying_function"): - return self.coords_to_point(x, graph.underlying_function(x)) - else: - # binary search - lh, rh = 0, 1 - while abs(lh - rh) > 0.001: - mh = np.mean([lh, rh]) - hands = [lh, mh, rh] - points = list(map(graph.point_from_proportion, hands)) - lx, mx, rx = list(map(self.x_axis.point_to_number, points)) - if lx <= x and rx >= x: - if mx > x: - rh = mh - else: - lh = mh - elif lx <= x and rx <= x: - return points[2] - elif lx >= x and rx >= x: - return points[0] - elif lx > x and rx < x: - lh, rh = rh, lh - return points[1] - return self.coords_to_point(x, graph.underlying_function(x)) + def get_axes(self): + return self.axes class ThreeDAxes(Axes): CONFIG = { + "dimension": 3, "x_min": -5.5, "x_max": 5.5, "y_min": -5.5, @@ -125,7 +161,7 @@ class ThreeDAxes(Axes): def __init__(self, **kwargs): Axes.__init__(self, **kwargs) - z_axis = self.z_axis = self.get_axis( + z_axis = self.z_axis = self.create_axis( self.z_min, self.z_max, self.z_axis_config ) z_axis.rotate(-np.pi / 2, UP, about_point=ORIGIN) @@ -133,18 +169,19 @@ class ThreeDAxes(Axes): angle_of_vector(self.z_normal), OUT, about_point=ORIGIN ) + self.axes.append(z_axis) self.add(z_axis) self.add_3d_pieces() self.set_axis_shading() def add_3d_pieces(self): - for axis in self: + for axis in self.axes: axis.pieces = VGroup( - *axis.main_line.get_pieces(self.num_axis_pieces) + *axis.get_pieces(self.num_axis_pieces) ) axis.add(axis.pieces) - axis.main_line.set_stroke(width=0, family=False) + axis.set_stroke(width=0, family=False) axis.set_shade_in_3d(True) def set_axis_shading(self): @@ -161,195 +198,180 @@ class ThreeDAxes(Axes): submob.set_sheen(0.2) -class NumberPlane(VMobject): +class NumberPlane(Axes): CONFIG = { - "color": BLUE_D, - "secondary_color": BLUE_E, - "axes_color": WHITE, - "secondary_stroke_width": 1, - # TODO: Allow coordinate center of NumberPlane to not be at (0, 0) - "x_radius": None, - "y_radius": None, - "x_unit_size": 1, - "y_unit_size": 1, - "center_point": ORIGIN, + "axis_config": { + "stroke_color": WHITE, + "stroke_width": 2, + "include_ticks": False, + "include_tip": False, + "line_to_number_buff": SMALL_BUFF, + "label_direction": DR, + "number_scale_val": 0.5, + }, + "y_axis_config": { + "label_direction": DR, + }, + "background_line_style": { + "stroke_color": BLUE_D, + "stroke_width": 2, + }, + # Defaults to a faded version of line_config + "faded_line_style": None, "x_line_frequency": 1, "y_line_frequency": 1, - "secondary_line_ratio": 1, - "written_coordinate_height": 0.2, - "propagate_style_to_family": False, + "faded_line_ratio": 1, "make_smooth_after_applying_functions": True, } - def generate_points(self): - if self.x_radius is None: - center_to_edge = (FRAME_X_RADIUS + abs(self.center_point[0])) - self.x_radius = center_to_edge / self.x_unit_size - if self.y_radius is None: - center_to_edge = (FRAME_Y_RADIUS + abs(self.center_point[1])) - self.y_radius = center_to_edge / self.y_unit_size - self.axes = VMobject() - self.main_lines = VMobject() - self.secondary_lines = VMobject() - tuples = [ - ( - self.x_radius, - self.x_line_frequency, - self.y_radius * DOWN, - self.y_radius * UP, - RIGHT - ), - ( - self.y_radius, - self.y_line_frequency, - self.x_radius * LEFT, - self.x_radius * RIGHT, - UP, - ), - ] - for radius, freq, start, end, unit in tuples: - main_range = np.arange(0, radius, freq) - step = freq / float(freq + self.secondary_line_ratio) - for v in np.arange(0, radius, step): - line1 = Line(start + v * unit, end + v * unit) - line2 = Line(start - v * unit, end - v * unit) - if v == 0: - self.axes.add(line1) - elif v in main_range: - self.main_lines.add(line1, line2) - else: - self.secondary_lines.add(line1, line2) - self.add(self.secondary_lines, self.main_lines, self.axes) - self.stretch(self.x_unit_size, 0) - self.stretch(self.y_unit_size, 1) - self.shift(self.center_point) - # Put x_axis before y_axis - y_axis, x_axis = self.axes.split() - self.axes = VMobject(x_axis, y_axis) + def __init__(self, **kwargs): + digest_config(self, kwargs) + kwargs["number_line_config"] = self.axis_config + Axes.__init__(self, **kwargs) + self.init_background_lines() - def init_colors(self): - VMobject.init_colors(self) - self.axes.set_stroke(self.axes_color, self.stroke_width) - self.main_lines.set_stroke(self.color, self.stroke_width) - self.secondary_lines.set_stroke( - self.secondary_color, self.secondary_stroke_width + def init_background_lines(self): + if self.faded_line_style is None: + background_line_style = self.background_line_style + color = background_line_style.get( + "stroke_color", WHITE + ) + stroke_width = background_line_style.get("stroke_width", 2) / 2 + self.faded_line_style = { + "stroke_color": color, + "stroke_width": stroke_width, + "stroke_opacity": 0.5, + } + + self.background_lines, self.faded_lines = self.get_lines() + self.background_lines.set_style( + **self.background_line_style, ) - return self + self.faded_lines.set_style( + **self.faded_line_style, + ) + self.add_to_back( + self.faded_lines, + self.background_lines, + ) + + def get_lines(self): + x_axis = self.get_x_axis() + y_axis = self.get_y_axis() + x_freq = self.x_line_frequency + y_freq = self.y_line_frequency + + x_lines1, x_lines2 = self.get_lines_parallel_to_axis( + x_axis, y_axis, x_freq, + self.faded_line_ratio, + ) + y_lines1, y_lines2 = self.get_lines_parallel_to_axis( + y_axis, x_axis, y_freq, + self.faded_line_ratio, + ) + lines1 = VGroup(*x_lines1, *y_lines1) + lines2 = VGroup(*x_lines2, *y_lines2) + return lines1, lines2 + + def get_lines_parallel_to_axis(self, axis1, axis2, freq, ratio): + line = Line(axis1.get_start(), axis1.get_end()) + vect = line.get_vector() + dense_freq = (1 + ratio) + step = 1 / dense_freq + + lines1 = VGroup() + lines2 = VGroup() + ranges = ( + np.arange(0, axis2.x_max, step), + np.arange(0, axis2.x_min, -step), + ) + for inputs in ranges: + for k, x in enumerate(inputs): + new_line = line.copy() + new_line.move_to(axis2.number_to_point(x)) + new_line.align_to(line, vect) + if k % (1 + ratio) == 0: + lines1.add(new_line) + else: + lines2.add(new_line) + return lines1, lines2 def get_center_point(self): return self.coords_to_point(0, 0) - def coords_to_point(self, x, y): - x, y = np.array([x, y]) - result = self.axes.get_center() - result += x * self.get_x_unit_size() * RIGHT - result += y * self.get_y_unit_size() * UP - return result - - def point_to_coords(self, point): - new_point = point - self.axes.get_center() - x = new_point[0] / self.get_x_unit_size() - y = new_point[1] / self.get_y_unit_size() - return x, y - - # Does not recompute center, unit_sizes for each call; useful for - # iterating over large lists of points, but does assume these - # attributes are kept accurate. (Could alternatively have a method - # which returns a function dynamically created after a single - # call to each of get_center(), get_x_unit_size(), etc.) - def point_to_coords_cheap(self, point): - new_point = point - self.center_point - x = new_point[0] / self.x_unit_size - y = new_point[1] / self.y_unit_size - return x, y - def get_x_unit_size(self): - return self.axes.get_width() / (2.0 * self.x_radius) + return self.get_x_axis().get_unit_size() def get_y_unit_size(self): - return self.axes.get_height() / (2.0 * self.y_radius) + return self.get_x_axis().get_unit_size() def get_coordinate_labels(self, x_vals=None, y_vals=None): - coordinate_labels = VGroup() - if x_vals is None: - x_vals = list(range(-int(self.x_radius), int(self.x_radius) + 1)) - if y_vals is None: - y_vals = list(range(-int(self.y_radius), int(self.y_radius) + 1)) - for index, vals in enumerate([x_vals, y_vals]): - num_pair = [0, 0] - for val in vals: - if val == 0: - continue - num_pair[index] = val - point = self.coords_to_point(*num_pair) - num = TexMobject(str(val)) - num.add_background_rectangle() - num.set_height( - self.written_coordinate_height - ) - num.next_to(point, DOWN + LEFT, buff=SMALL_BUFF) - coordinate_labels.add(num) - self.coordinate_labels = coordinate_labels - return coordinate_labels + x_vals = x_vals or [] + y_vals = y_vals or [] + x_mobs = self.get_x_axis().get_number_mobjects(*x_vals) + y_mobs = self.get_y_axis().get_number_mobjects(*y_vals) + + self.coordinate_labels = VGroup(x_mobs, y_mobs) + return self.coordinate_labels def get_axes(self): return self.axes - def get_axis_labels(self, x_label="x", y_label="y"): - x_axis, y_axis = self.get_axes().split() - quads = [ - (x_axis, x_label, UP, RIGHT), - (y_axis, y_label, RIGHT, UP), - ] - labels = VGroup() - for axis, tex, vect, edge in quads: - label = TexMobject(tex) - label.add_background_rectangle() - label.next_to(axis, vect) - label.to_edge(edge) - labels.add(label) - self.axis_labels = labels - return labels + def get_x_axis_label(self, label_tex, edge=RIGHT, direction=DL, **kwargs): + return self.get_axis_label( + label_tex, self.get_x_axis(), + edge, direction, **kwargs + ) + + def get_y_axis_label(self, label_tex, edge=UP, direction=DR, **kwargs): + return self.get_axis_label( + label_tex, self.get_y_axis(), + edge, direction, **kwargs + ) + + def get_axis_label(self, label_tex, axis, edge, direction, buff=MED_SMALL_BUFF): + label = TexMobject(label_tex) + label.next_to( + axis.get_edge_center(edge), direction, + buff=buff + ) + return label + + def get_axis_labels(self, x_label_tex="x", y_label_tex="y"): + self.axis_labels = VGroup( + self.get_x_axis_label(x_label_tex), + self.get_y_axis_label(y_label_tex), + ) + return self.axis_labels def add_coordinates(self, x_vals=None, y_vals=None): - self.add(*self.get_coordinate_labels(x_vals, y_vals)) + self.add(self.get_coordinate_labels(x_vals, y_vals)) return self def get_vector(self, coords, **kwargs): - point = coords[0] * RIGHT + coords[1] * UP - arrow = Arrow(ORIGIN, point, **kwargs) - return arrow + kwargs["buff"] = 0 + return Arrow( + self.coords_to_point(0, 0), + self.coords_to_point(*coords), + **kwargs + ) def prepare_for_nonlinear_transform(self, num_inserted_curves=50): for mob in self.family_members_with_points(): num_curves = mob.get_num_curves() if num_inserted_curves > num_curves: mob.insert_n_curves( - num_inserted_curves - num_curves) - mob.make_smooth() + num_inserted_curves - num_curves + ) return self class ComplexPlane(NumberPlane): CONFIG = { "color": BLUE, - "unit_size": 1, "line_frequency": 1, - "faded_line_frequency": 0.5, } - def __init__(self, **kwargs): - digest_config(self, kwargs) - kwargs.update({ - "x_unit_size": self.unit_size, - "y_unit_size": self.unit_size, - "x_line_frequency": self.line_frequency, - "x_faded_line_frequency": self.faded_line_frequency, - "y_line_frequency": self.line_frequency, - "y_faded_line_frequency": self.faded_line_frequency, - }) - NumberPlane.__init__(self, **kwargs) - def number_to_point(self, number): number = complex(number) return self.coords_to_point(number.real, number.imag) @@ -358,35 +380,35 @@ class ComplexPlane(NumberPlane): x, y = self.point_to_coords(point) return complex(x, y) - def get_coordinate_labels(self, *numbers): - # TODO: Should merge this with the code from NumberPlane.get_coordinate_labels + def get_default_coordinate_values(self): + x_numbers = self.get_x_axis().default_numbers_to_display() + y_numbers = self.get_y_axis().default_numbers_to_display() + y_numbers = [ + complex(0, y) for y in y_numbers if y != 0 + ] + return [*x_numbers, *y_numbers] - result = VGroup() + def get_coordinate_labels(self, *numbers, **kwargs): if len(numbers) == 0: - numbers = list(range(-int(self.x_radius), int(self.x_radius) + 1)) - numbers += [ - complex(0, y) - for y in range(-int(self.y_radius), int(self.y_radius) + 1) - if y != 0 - ] + numbers = self.get_default_coordinate_values() + + self.coordinate_labels = VGroup() for number in numbers: - # if number == complex(0, 0): - # continue - point = self.number_to_point(number) - num_str = str(number).replace("j", "i") - if num_str.startswith("0"): - num_str = "0" - elif num_str in ["1i", "-1i"]: - num_str = num_str.replace("1", "") - num_mob = TexMobject(num_str) - num_mob.add_background_rectangle() - num_mob.set_height(self.written_coordinate_height) - num_mob.next_to(point, DOWN + LEFT, SMALL_BUFF) - result.add(num_mob) - self.coordinate_labels = result - return result + z = complex(number) + if abs(z.imag) > abs(z.real): + axis = self.get_y_axis() + value = z.imag + kwargs = merge_config([ + {"number_config": {"unit": "i"}}, + kwargs, + ]) + else: + axis = self.get_x_axis() + value = z.real + number_mob = axis.get_number_mobject(value, **kwargs) + self.coordinate_labels.add(number_mob) + return self.coordinate_labels def add_coordinates(self, *numbers): - self.coordinate_labels = self.get_coordinate_labels(*numbers) - self.add(self.coordinate_labels) + self.add(self.get_coordinate_labels(*numbers)) return self diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 0f34ecad..40d202aa 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -36,11 +36,9 @@ class Mobject(Container): "target": None, } - def __init__(self, *submobjects, **kwargs): - Container.__init__(self, *submobjects, **kwargs) - if not all([isinstance(m, Mobject) for m in submobjects]): - raise Exception("All submobjects must be of type Mobject") - self.submobjects = list(submobjects) + def __init__(self, **kwargs): + Container.__init__(self, **kwargs) + self.submobjects = [] self.color = Color(self.color) if self.name is None: self.name = self.__class__.__name__ @@ -1031,7 +1029,8 @@ class Mobject(Container): class Group(Mobject): - # Alternate name to improve readibility in cases where - # the mobject is used primarily for its submobject housing - # functionality. - pass + def __init__(self, *mobjects, **kwargs): + if not all([isinstance(m, Mobject) for m in mobjects]): + raise Exception("All submobjects must be of type Mobject") + Mobject.__init__(self, **kwargs) + self.add(*mobjects) diff --git a/manimlib/mobject/number_line.py b/manimlib/mobject/number_line.py index 4eb86c95..8b65feab 100644 --- a/manimlib/mobject/number_line.py +++ b/manimlib/mobject/number_line.py @@ -1,25 +1,30 @@ +import operator as op + from manimlib.constants import * -from manimlib.mobject.geometry import Arrow +from manimlib.mobject.geometry import RegularPolygon from manimlib.mobject.geometry import Line from manimlib.mobject.numbers import DecimalNumber from manimlib.mobject.types.vectorized_mobject import VGroup -from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.bezier import interpolate from manimlib.utils.config_ops import digest_config +from manimlib.utils.config_ops import merge_config from manimlib.utils.simple_functions import fdiv -from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import normalize -class NumberLine(VMobject): +class NumberLine(Line): CONFIG = { "color": LIGHT_GREY, "x_min": -FRAME_X_RADIUS, "x_max": FRAME_X_RADIUS, "unit_size": 1, + "include_ticks": True, "tick_size": 0.1, "tick_frequency": 1, - "leftmost_tick": None, # Defaults to value near x_min s.t. 0 is a tick + # Defaults to value near x_min s.t. 0 is a tick + # TODO, rename this + "leftmost_tick": None, + # Change name "numbers_with_elongated_ticks": [0], "include_numbers": False, "numbers_to_show": None, @@ -29,6 +34,8 @@ class NumberLine(VMobject): "label_direction": DOWN, "line_to_number_buff": MED_SMALL_BUFF, "include_tip": False, + "tip_width": 0.25, + "tip_height": 0.25, "decimal_number_config": { "num_decimal_places": 0, } @@ -36,68 +43,72 @@ class NumberLine(VMobject): def __init__(self, **kwargs): digest_config(self, kwargs) - if self.leftmost_tick is None: - tf = self.tick_frequency - self.leftmost_tick = tf * np.ceil(self.x_min / tf) - VMobject.__init__(self, **kwargs) + start = self.unit_size * self.x_min * RIGHT + end = self.unit_size * self.x_max * RIGHT + Line.__init__(self, start, end, **kwargs) + self.shift(-self.number_to_point(self.number_at_center)) + + self.init_leftmost_tick() + if self.include_ticks: + self.add_tick_marks() if self.include_tip: self.add_tip() if self.include_numbers: self.add_numbers() - def generate_points(self): - self.main_line = Line(self.x_min * RIGHT, self.x_max * RIGHT) - self.tick_marks = VGroup() - self.add(self.main_line, self.tick_marks) - rounding_value = int(-np.log10(0.1 * self.tick_frequency)) - rounded_numbers_with_elongated_ticks = np.round( - self.numbers_with_elongated_ticks, - rounding_value + def init_leftmost_tick(self): + if self.leftmost_tick is None: + self.leftmost_tick = op.mul( + self.tick_frequency, + np.ceil(self.x_min / self.tick_frequency) + ) + + def add_tick_marks(self): + tick_size = self.tick_size + self.tick_marks = VGroup(*[ + self.get_tick(x, tick_size) + for x in self.get_tick_numbers() + ]) + big_tick_size = tick_size * self.longer_tick_multiple + self.big_tick_marks = VGroup(*[ + self.get_tick(x, big_tick_size) + for x in self.numbers_with_elongated_ticks + ]) + self.add( + self.tick_marks, + self.big_tick_marks, ) - for x in self.get_tick_numbers(): - rounded_x = np.round(x, rounding_value) - if rounded_x in rounded_numbers_with_elongated_ticks: - tick_size_used = self.longer_tick_multiple * self.tick_size - else: - tick_size_used = self.tick_size - self.add_tick(x, tick_size_used) - - self.stretch(self.unit_size, 0) - self.shift(-self.number_to_point(self.number_at_center)) - - def add_tick(self, x, size=None): - self.tick_marks.add(self.get_tick(x, size)) - return self - def get_tick(self, x, size=None): if size is None: size = self.tick_size result = Line(size * DOWN, size * UP) - result.rotate(self.main_line.get_angle()) + result.rotate(self.get_angle()) result.move_to(self.number_to_point(x)) + result.match_style(self) return result def get_tick_marks(self): - return self.tick_marks + return VGroup( + *self.tick_marks, + *self.big_tick_marks, + ) def get_tick_numbers(self): - epsilon = 0.001 return np.arange( - self.leftmost_tick, self.x_max + epsilon, + self.leftmost_tick, + self.x_max + self.tick_frequency / 2, self.tick_frequency ) def number_to_point(self, number): alpha = float(number - self.x_min) / (self.x_max - self.x_min) return interpolate( - self.main_line.get_start(), - self.main_line.get_end(), - alpha + self.get_start(), self.get_end(), alpha ) def point_to_number(self, point): - start_point, end_point = self.main_line.get_start_and_end() + start_point, end_point = self.get_start_and_end() full_vect = end_point - start_point unit_vect = normalize(full_vect) @@ -110,28 +121,43 @@ class NumberLine(VMobject): ) return interpolate(self.x_min, self.x_max, proportion) + def get_unit_size(self): + return (self.x_max - self.x_min) / self.get_length() + def default_numbers_to_display(self): if self.numbers_to_show is not None: return self.numbers_to_show return np.arange(int(self.leftmost_tick), int(self.x_max) + 1) + def get_number_mobject(self, number, + number_config=None, + scale_val=None, + direction=None, + buff=None): + number_config = merge_config([ + number_config or {}, + self.decimal_number_config + ]) + scale_val = scale_val or self.number_scale_val + direction = direction or self.label_direction + buff = buff or self.line_to_number_buff + + num_mob = DecimalNumber(number, **number_config) + num_mob.scale(scale_val) + num_mob.next_to( + self.number_to_point(number), + direction=direction, + buff=buff + ) + return num_mob + def get_number_mobjects(self, *numbers, **kwargs): - # TODO, handle decimals if len(numbers) == 0: numbers = self.default_numbers_to_display() - result = VGroup() - for number in numbers: - mob = DecimalNumber( - number, **self.decimal_number_config - ) - mob.scale(self.number_scale_val) - mob.next_to( - self.number_to_point(number), - self.label_direction, - self.line_to_number_buff, - ) - result.add(mob) - return result + return VGroup(*[ + self.get_number_mobject(number, **kwargs) + for number in numbers + ]) def get_labels(self): return self.get_number_mobjects() @@ -144,12 +170,13 @@ class NumberLine(VMobject): return self def add_tip(self): - start, end = self.main_line.get_start_and_end() - vect = (end - start) / get_norm(end - start) - arrow = Arrow(start, end + MED_SMALL_BUFF * vect, buff=0) - tip = arrow.tip - tip.set_stroke(width=self.get_stroke_width()) - tip.set_color(self.color) + tip = RegularPolygon(3) + color = self.color + tip.set_stroke(color, width=self.get_stroke_width()) + tip.set_fill(color, opacity=1) + tip.set_width(self.tip_width) + tip.set_height(self.tip_height, stretch=True) + tip.move_to(self.get_end(), LEFT) self.tip = tip self.add(tip) diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index eebee8d6..22ed550b 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -26,7 +26,7 @@ class DecimalNumber(VMobject): formatter = self.get_formatter() num_string = formatter.format(number) - rounded_num = np.round(float(number), self.num_decimal_places) + rounded_num = np.round(number, self.num_decimal_places) if num_string.startswith("-") and rounded_num == 0: if self.include_sign: num_string = "+" + num_string[1:] diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index 7384308d..5584753b 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -11,7 +11,6 @@ from manimlib.mobject.geometry import Rectangle from manimlib.mobject.geometry import RoundedRectangle from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject -from manimlib.utils.bezier import is_closed from manimlib.utils.color import * from manimlib.utils.config_ops import digest_config from manimlib.utils.config_ops import digest_locals @@ -105,14 +104,14 @@ class SVGMobject(VMobject): pass # TODO # warnings.warn("Unknown element type: " + element.tagName) result = [m for m in result if m is not None] - self.handle_transforms(element, VMobject(*result)) + self.handle_transforms(element, VGroup(*result)) if len(result) > 1 and not self.unpack_groups: result = [VGroup(*result)] return result def g_to_mobjects(self, g_element): - mob = VMobject(*self.get_mobjects_from(g_element)) + mob = VGroup(*self.get_mobjects_from(g_element)) self.handle_transforms(g_element, mob) return mob.submobjects @@ -124,7 +123,7 @@ class SVGMobject(VMobject): ref = use_element.getAttribute("xlink:href")[1:] if ref not in self.ref_to_element: warnings.warn("%s not recognized" % ref) - return VMobject() + return VGroup() return self.get_mobjects_from( self.ref_to_element[ref] ) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index f27eda8e..d63ed81e 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -796,18 +796,11 @@ class VMobject(Mobject): class VGroup(VMobject): - def __init__(self, *args, **kwargs): - if len(args) == 1 and isinstance(args[0], (tuple, list)): - args = args[0] - - packed_args = [] - for arg in args: - if isinstance(arg, (tuple, list)): - packed_args.append(VGroup(arg)) - else: - packed_args.append(arg) - - VMobject.__init__(self, *packed_args, **kwargs) + def __init__(self, *vmobjects, **kwargs): + if not all([isinstance(m, VMobject) for m in vmobjects]): + raise Exception("All submobjects must be of type VMobject") + VMobject.__init__(self, **kwargs) + self.add(*vmobjects) class VectorizedPoint(VMobject): diff --git a/manimlib/once_useful_constructs/complex_transformation_scene.py b/manimlib/once_useful_constructs/complex_transformation_scene.py index b6b88208..b21747dd 100644 --- a/manimlib/once_useful_constructs/complex_transformation_scene.py +++ b/manimlib/once_useful_constructs/complex_transformation_scene.py @@ -96,7 +96,7 @@ class ComplexTransformationScene(Scene): # TODO... def paint_plane(self, plane): - for lines in plane.main_lines, plane.secondary_lines: + for lines in planes, plane.secondary_lines: lines.set_color_by_gradient( self.vert_start_color, self.vert_end_color, diff --git a/manimlib/scene/three_d_scene.py b/manimlib/scene/three_d_scene.py index 1ccdb9c2..8396a2a9 100644 --- a/manimlib/scene/three_d_scene.py +++ b/manimlib/scene/three_d_scene.py @@ -156,10 +156,10 @@ class SpecialThreeDScene(ThreeDScene): axes = ThreeDAxes(**self.three_d_axes_config) for axis in axes: if self.cut_axes_at_radius: - p0 = axis.main_line.get_start() + p0 = axis.get_start() p1 = axis.number_to_point(-1) p2 = axis.number_to_point(1) - p3 = axis.main_line.get_end() + p3 = axis.get_end() new_pieces = VGroup( Line(p0, p1), Line(p1, p2), Line(p2, p3), ) diff --git a/manimlib/utils/config_ops.py b/manimlib/utils/config_ops.py index e21ba1ac..37be72a7 100644 --- a/manimlib/utils/config_ops.py +++ b/manimlib/utils/config_ops.py @@ -56,12 +56,16 @@ def digest_config(obj, kwargs, caller_locals={}): obj.__dict__ = merge_config(all_dicts) +# TODO, priority here is backwards from dict.update. +# Should I change the convention? def merge_config(all_dicts): """ Creates a dict whose keyset is the union of all the input dictionaries. The value for each key is based on the first dict in the list with that key. + First dicts have higher priority + When values are dictionaries, it is applied recursively """ all_config = reduce(op.add, [list(d.items()) for d in all_dicts]) diff --git a/manimlib/utils/simple_functions.py b/manimlib/utils/simple_functions.py index 797a09cc..f6192267 100644 --- a/manimlib/utils/simple_functions.py +++ b/manimlib/utils/simple_functions.py @@ -62,3 +62,30 @@ def fdiv(a, b, zero_over_zero_value=None): where = True return np.true_divide(a, b, out=out, where=where) + + +def binary_search(function, + target, + lower_bound, + upper_bound, + tolerance=1e-4): + lh = lower_bound + rh = upper_bound + while abs(rh - lh) > tolerance: + mh = np.mean([lh, rh]) + lx, mx, rx = [function(h) for h in (lh, mh, rh)] + if lx == target: + return lx + if rx == target: + return rx + + if lx <= target and rx >= target: + if mx > target: + rh = mh + else: + lh = mh + elif lx > target and rx < target: + lh, rh = rh, lh + else: + return None + return mh diff --git a/old_projects/WindingNumber_G.py b/old_projects/WindingNumber_G.py index 2666a5ac..637e556b 100644 --- a/old_projects/WindingNumber_G.py +++ b/old_projects/WindingNumber_G.py @@ -781,7 +781,7 @@ class InputOutputScene(Scene): plane.add_coordinates(x_vals = list(range(-2, 3)), y_vals = list(range(-2, 3))) plane.white_parts = VGroup(plane.axes, plane.coordinate_labels) plane.coordinate_labels.set_background_stroke(width=0) - plane.lines_to_fade = VGroup(plane.main_lines, plane.secondary_lines) + plane.lines_to_fade = VGroup(planes, plane.secondary_lines) plane.move_to(vect*FRAME_X_RADIUS/2 + self.y_shift*DOWN) label = TextMobject(text) label.scale(1.5) diff --git a/old_projects/alt_calc.py b/old_projects/alt_calc.py index b2612fe4..b470d435 100644 --- a/old_projects/alt_calc.py +++ b/old_projects/alt_calc.py @@ -76,7 +76,7 @@ class NumberlineTransformationScene(ZoomedScene): full_config = dict(self.number_line_config) full_config.update(added_config) number_line = NumberLine(**full_config) - number_line.main_line.insert_n_curves( + number_line.insert_n_curves( self.num_inserted_number_line_curves ) number_line.shift(zero_point - number_line.number_to_point(0)) @@ -179,12 +179,12 @@ class NumberlineTransformationScene(ZoomedScene): self.moving_input_line = input_line_copy input_line_copy.remove(input_line_copy.numbers) # input_line_copy.set_stroke(width=2) - input_line_copy.main_line.insert_n_curves( + input_line_copy.insert_n_curves( self.num_inserted_number_line_curves ) return AnimationGroup( self.get_mapping_animation( - func, input_line_copy.main_line, + func, input_line_copy, apply_function_to_points ), self.get_mapping_animation( @@ -242,7 +242,7 @@ class NumberlineTransformationScene(ZoomedScene): zoom_anim.update(1) target_mini_line = Line(frame.get_left(), frame.get_right()) target_mini_line.scale(self.mini_line_scale_factor) - target_mini_line.match_style(self.output_line.main_line) + target_mini_line.match_style(self.output_line) zoom_anim.update(0) zcbr_group.submobjects.insert(1, target_mini_line) if target_coordinate_values: @@ -312,7 +312,7 @@ class NumberlineTransformationScene(ZoomedScene): mini_line = self.mini_line = Line(frame.get_left(), frame.get_right()) mini_line.scale(self.mini_line_scale_factor) mini_line.insert_n_curves(self.num_inserted_number_line_curves) - mini_line.match_style(self.input_line.main_line) + mini_line.match_style(self.input_line) mini_line_copy = mini_line.copy() zcbr_group.add(mini_line_copy, mini_line) anims += [FadeIn(mini_line), FadeIn(mini_line_copy)] @@ -2935,7 +2935,7 @@ class AnalyzeFunctionWithTransformations(NumberlineTransformationScene): def setup_number_lines(self): NumberlineTransformationScene.setup_number_lines(self) for line in self.input_line, self.output_line: - VGroup(line.main_line, line.tick_marks).set_stroke(width=2) + VGroup(line, line.tick_marks).set_stroke(width=2) def add_function_title(self): title = TexMobject("f(x)", "=", "1 +", "\\frac{1}{x}") diff --git a/old_projects/basel/basel2.py b/old_projects/basel/basel2.py index e25b3954..eae0ab16 100644 --- a/old_projects/basel/basel2.py +++ b/old_projects/basel/basel2.py @@ -3883,7 +3883,7 @@ class ThinkBackToHowAmazingThisIs(ThreeDScene): self.number_line = number_line def show_giant_circle(self): - self.number_line.main_line.insert_n_curves(10000) + self.number_line.insert_n_curves(10000) everything = VGroup(*self.mobjects) circle = everything.copy() circle.move_to(ORIGIN) diff --git a/old_projects/div_curl.py b/old_projects/div_curl.py index 4a41aac0..afff0dfb 100644 --- a/old_projects/div_curl.py +++ b/old_projects/div_curl.py @@ -3223,7 +3223,7 @@ class PhaseSpaceOfPopulationModel(ShowTwoPopulations, PiCreatureScene, MovingCam ]) for axis, label, vect in zip(axes, axes_labels, [RIGHT, UP]): label.next_to( - axis.main_line, vect, + axis, vect, submobject_to_align=label[0] ) @@ -4297,7 +4297,7 @@ class ZToHalfFlowNearWall(ComplexTransformationScene, MovingCameraScene): secondary_line_ratio=0, ) plane.next_to(ORIGIN, UP, buff=0.001) - horizontal_lines = VGroup(*[l for l in list(plane.main_lines) + [plane.axes[0]] if np.abs(l.get_center()[0]) < 0.1]) + horizontal_lines = VGroup(*[l for l in list(planes) + [plane.axes[0]] if np.abs(l.get_center()[0]) < 0.1]) plane.set_stroke(MAROON_B, width=2) horizontal_lines.set_stroke(BLUE, width=2) diff --git a/old_projects/eoc/chapter6.py b/old_projects/eoc/chapter6.py index 3f9f0c15..6148e07c 100644 --- a/old_projects/eoc/chapter6.py +++ b/old_projects/eoc/chapter6.py @@ -65,7 +65,7 @@ class SlopeOfCircleExample(ZoomedScene): def setup_plane(self): self.plane = NumberPlane(**self.plane_kwargs) - self.plane.main_lines.fade() + self.planes.fade() self.plane.add(self.plane.get_axis_labels()) self.plane.add_coordinates() diff --git a/old_projects/eoc/chapter8.py b/old_projects/eoc/chapter8.py index 9fce8daa..c2cbfe69 100644 --- a/old_projects/eoc/chapter8.py +++ b/old_projects/eoc/chapter8.py @@ -652,7 +652,7 @@ class ConstantVelocityPlot(PlotVelocity): def note_units(self): x_line, y_line = lines = VGroup(*[ - axis.main_line.copy() + axis.copy() for axis in (self.x_axis, self.y_axis) ]) lines.set_color(TIME_COLOR) diff --git a/old_projects/eola/chapter10.py b/old_projects/eola/chapter10.py index 1118fcac..6bccaf27 100644 --- a/old_projects/eola/chapter10.py +++ b/old_projects/eola/chapter10.py @@ -2221,7 +2221,7 @@ class ChangeToEigenBasis(ExampleTranformationScene): self.play(FadeOut(self.plane)) cob_transform = self.get_matrix_transformation([[1, 0], [-1, 1]]) ApplyMethod(self.plane.apply_function, cob_transform).update(1) - self.plane.main_lines.set_color(BLUE_D) + self.planes.set_color(BLUE_D) self.plane.axes.set_color(WHITE) self.play( FadeIn(self.plane), diff --git a/old_projects/fourier.py b/old_projects/fourier.py index 3979cd0d..711e6516 100644 --- a/old_projects/fourier.py +++ b/old_projects/fourier.py @@ -2047,8 +2047,8 @@ class ShowCommutativeDiagram(ShowLinearity): VGroup(ta_group[1], fa_group[1]).shift(MED_LARGE_BUFF*UP) for ta, fa in zip(ta_group, fa_group): fa.next_to( - ta.x_axis.main_line, RIGHT, - submobject_to_align = fa.x_axis.main_line + ta.x_axis, RIGHT, + submobject_to_align = fa.x_axis ) fa.to_edge(RIGHT) ta.remove(ta.labels) @@ -2084,7 +2084,7 @@ class ShowCommutativeDiagram(ShowLinearity): fourier_graph.set_color(self.center_of_mass_color) arrow = Arrow( - ta.x_axis.main_line, fa.x_axis.main_line, + ta.x_axis, fa.x_axis, color = WHITE, buff = MED_LARGE_BUFF, ) @@ -3712,7 +3712,7 @@ class SummarizeTheFullTransform(DrawFrequencyPlot): ) imaginary_fourier_graph.set_color(BLUE) imaginary_fourier_graph.shift( - frequency_axes.x_axis.main_line.get_right() - \ + frequency_axes.x_axis.get_right() - \ imaginary_fourier_graph.points[-1], ) diff --git a/old_projects/highD.py b/old_projects/highD.py index aebb5084..113eb5b0 100644 --- a/old_projects/highD.py +++ b/old_projects/highD.py @@ -46,7 +46,7 @@ class Slider(NumberLine): def add_label(self, tex): label = TexMobject(tex) label.scale(self.label_scale_val) - label.move_to(self.main_line.get_top()) + label.move_to(self.get_top()) label.shift(MED_LARGE_BUFF*UP) self.add(label) self.label = label diff --git a/old_projects/pi_day.py b/old_projects/pi_day.py index c19110d0..1468bd56 100644 --- a/old_projects/pi_day.py +++ b/old_projects/pi_day.py @@ -674,7 +674,7 @@ class QuarterTurn(Scene): class UsingTheta(Scene): def construct(self): plane = NumberPlane(x_unit_size = 3, y_unit_size = 3) - # plane.main_lines.fade(0.5) + # planes.fade(0.5) # plane.secondary_lines.fade(0.5) plane.fade(0.5) self.add(plane) diff --git a/old_projects/quaternions.py b/old_projects/quaternions.py index 921455c1..d51ea676 100644 --- a/old_projects/quaternions.py +++ b/old_projects/quaternions.py @@ -1245,7 +1245,7 @@ class IntroduceLinusTheLinelander(Scene): algebra.shift(3 * RIGHT) self.play( - ShowCreation(number_line.main_line), + ShowCreation(number_line), linus.look_at, number_line ) self.play( @@ -3123,9 +3123,9 @@ class IntroduceThreeDNumbers(SpecialThreeDScene): z_axis.set_color(WHITE) z_axis_top = Line( z_axis.number_to_point(0), - z_axis.main_line.get_end(), + z_axis.get_end(), ) - z_axis_top.match_style(z_axis.main_line) + z_axis_top.match_style(z_axis) z_unit_line = Line( z_axis.number_to_point(0), @@ -3155,7 +3155,7 @@ class IntroduceThreeDNumbers(SpecialThreeDScene): colored_coord_lines = VGroup(colored_, y_line, z_line) coord_lines = VGroup( - plane.axes[0], plane.axes[1], z_axis.main_line, + plane.axes[0], plane.axes[1], z_axis, ) for i1, i2 in [(0, 2), (1, 0), (2, 1)]: coord_lines[i1].target = coord_lines[i2].copy() diff --git a/old_projects/triples.py b/old_projects/triples.py index 7a3cca2f..68959266 100644 --- a/old_projects/triples.py +++ b/old_projects/triples.py @@ -1700,7 +1700,7 @@ class VisualizeZSquared(Scene): color_grid = self.get_color_grid() self.play( - self.background_plane.main_lines.set_stroke, None, 1, + self.background_planes.set_stroke, None, 1, LaggedStart( FadeIn, color_grid, run_time = 2 @@ -1807,7 +1807,7 @@ class VisualizeZSquared(Scene): secondary_line_ratio = 0, stroke_width = 2, ) - color_grid.main_lines.set_color_by_gradient( + color_grids.set_color_by_gradient( *[GREEN, RED, MAROON_B, TEAL]*2 ) color_grid.remove(color_grid.axes[0]) diff --git a/old_projects/uncertainty.py b/old_projects/uncertainty.py index 2ad59384..30f80218 100644 --- a/old_projects/uncertainty.py +++ b/old_projects/uncertainty.py @@ -877,7 +877,7 @@ class TwoCarsAtRedLight(Scene): )) self.play( ApplyMethod( - self.time_axes.x_axis.main_line.stretch, 2.5, 0, + self.time_axes.x_axis.stretch, 2.5, 0, {"about_edge" : LEFT}, run_time = 4, rate_func = squish_rate_func(smooth, 0.3, 0.6), @@ -885,7 +885,7 @@ class TwoCarsAtRedLight(Scene): UpdateFromFunc( self.time_axes.x_axis.tip, lambda m : m.move_to( - self.time_axes.x_axis.main_line.get_right(), + self.time_axes.x_axis.get_right(), LEFT ) ), @@ -1364,7 +1364,7 @@ class CenterOfMassDescription(FourierRecapScene): circle_plane.target.set_height(FRAME_HEIGHT) circle_plane.target.center() circle_plane.target.axes.set_stroke(width = 2) - circle_plane.target.main_lines.set_stroke(width = 2) + circle_plane.targets.set_stroke(width = 2) circle_plane.target.secondary_lines.set_stroke(width = 1) start_coords = (0.5, 0.5) @@ -2940,7 +2940,7 @@ class IntroduceDeBroglie(Scene): self.wait() #Transform time_line - line = time_line.main_line + line = time_line self.play( FadeOut(time_line.numbers), VGroup(arrow, words, date).shift, MED_LARGE_BUFF*UP,