diff --git a/_2015/generate_logo.py b/_2015/generate_logo.py index 48268ce..6e4ccfb 100644 --- a/_2015/generate_logo.py +++ b/_2015/generate_logo.py @@ -3,6 +3,16 @@ from manim_imports_ext import * ## Warning, much of what is in this class ## likely not supported anymore. + +def drag_pixels(frames): + curr = frames[0] + new_frames = [] + for frame in frames: + curr += (curr == 0) * np.array(frame) + new_frames.append(np.array(curr)) + return new_frames + + class LogoGeneration(Scene): CONFIG = { "radius" : 1.5, diff --git a/once_useful_constructs/arithmetic.py b/once_useful_constructs/arithmetic.py new file mode 100644 index 0000000..ba53edf --- /dev/null +++ b/once_useful_constructs/arithmetic.py @@ -0,0 +1,107 @@ +import numpy as np + +from manimlib.animation.animation import Animation +from manimlib.mobject.mobject import Mobject +from manimlib.constants import * +from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.scene.scene import Scene +from manimlib.utils.paths import path_along_arc + + +class RearrangeEquation(Scene): + def construct( + self, + start_terms, + end_terms, + index_map, + path_arc=np.pi, + start_transform=None, + end_transform=None, + leave_start_terms=False, + transform_kwargs={}, + ): + transform_kwargs["path_func"] = path_along_arc(path_arc) + start_mobs, end_mobs = self.get_mobs_from_terms( + start_terms, end_terms + ) + if start_transform: + start_mobs = start_transform(Mobject(*start_mobs)).split() + if end_transform: + end_mobs = end_transform(Mobject(*end_mobs)).split() + unmatched_start_indices = set(range(len(start_mobs))) + unmatched_end_indices = set(range(len(end_mobs))) + unmatched_start_indices.difference_update( + [n % len(start_mobs) for n in index_map] + ) + unmatched_end_indices.difference_update( + [n % len(end_mobs) for n in list(index_map.values())] + ) + mobject_pairs = [ + (start_mobs[a], end_mobs[b]) + for a, b in index_map.items() + ] + [ + (Point(end_mobs[b].get_center()), end_mobs[b]) + for b in unmatched_end_indices + ] + if not leave_start_terms: + mobject_pairs += [ + (start_mobs[a], Point(start_mobs[a].get_center())) + for a in unmatched_start_indices + ] + + self.add(*start_mobs) + if leave_start_terms: + self.add(Mobject(*start_mobs)) + self.wait() + self.play(*[ + Transform(*pair, **transform_kwargs) + for pair in mobject_pairs + ]) + self.wait() + + def get_mobs_from_terms(self, start_terms, end_terms): + """ + Need to ensure that all image mobjects for a tex expression + stemming from the same string are point-for-point copies of one + and other. This makes transitions much smoother, and not look + like point-clouds. + """ + num_start_terms = len(start_terms) + all_mobs = np.array( + Tex(start_terms).split() + Tex(end_terms).split()) + all_terms = np.array(start_terms + end_terms) + for term in set(all_terms): + matches = all_terms == term + if sum(matches) > 1: + base_mob = all_mobs[list(all_terms).index(term)] + all_mobs[matches] = [ + base_mob.copy().replace(target_mob) + for target_mob in all_mobs[matches] + ] + return all_mobs[:num_start_terms], all_mobs[num_start_terms:] + + +class FlipThroughSymbols(Animation): + def __init__( + self, + tex_list, + start_center=ORIGIN, + end_center=ORIGIN, + **kwargs + ): + self.tex_list = tex_list + self.start_center = start_center + self.end_center = end_center + mobject = Tex(self.curr_tex).shift(start_center) + Animation.__init__(self, mobject, **kwargs) + + def interpolate_mobject(self, alpha): + new_tex = self.tex_list[np.ceil(alpha * len(self.tex_list)) - 1] + + if new_tex != self.curr_tex: + self.curr_tex = new_tex + self.mobject = Tex(new_tex).shift(self.start_center) + if not all(self.start_center == self.end_center): + self.mobject.center().shift( + (1 - alpha) * self.start_center + alpha * self.end_center + ) diff --git a/so_old_as_to_ignore/butterfly_curve.py b/once_useful_constructs/butterfly_curve.py similarity index 100% rename from so_old_as_to_ignore/butterfly_curve.py rename to once_useful_constructs/butterfly_curve.py diff --git a/once_useful_constructs/combinatorics.py b/once_useful_constructs/combinatorics.py new file mode 100644 index 0000000..5fc090b --- /dev/null +++ b/once_useful_constructs/combinatorics.py @@ -0,0 +1,184 @@ +from manimlib.constants import * +from manimlib.mobject.numbers import Integer +from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.mobject.types.vectorized_mobject import VMobject, VGroup +from manimlib.scene.scene import Scene +from manimlib.utils.simple_functions import choose + + +DEFAULT_COUNT_NUM_OFFSET = (FRAME_X_RADIUS - 1, FRAME_Y_RADIUS - 1, 0) +DEFAULT_COUNT_RUN_TIME = 5.0 + + +class CountingScene(Scene): + def count(self, items, item_type="mobject", *args, **kwargs): + if item_type == "mobject": + self.count_mobjects(items, *args, **kwargs) + elif item_type == "region": + self.count_regions(items, *args, **kwargs) + else: + raise Exception("Unknown item_type, should be mobject or region") + return self + + def count_mobjects( + self, mobjects, mode="highlight", + color="red", + display_numbers=True, + num_offset=DEFAULT_COUNT_NUM_OFFSET, + run_time=DEFAULT_COUNT_RUN_TIME, + ): + """ + Note, leaves final number mobject as "number" attribute + + mode can be "highlight", "show_creation" or "show", otherwise + a warning is given and nothing is animating during the count + """ + if len(mobjects) > 50: # TODO + raise Exception("I don't know if you should be counting \ + too many mobjects...") + if len(mobjects) == 0: + raise Exception("Counting mobject list of length 0") + if mode not in ["highlight", "show_creation", "show"]: + raise Warning("Unknown mode") + frame_time = run_time / len(mobjects) + if mode == "highlight": + self.add(*mobjects) + for mob, num in zip(mobjects, it.count(1)): + if display_numbers: + num_mob = Tex(str(num)) + num_mob.center().shift(num_offset) + self.add(num_mob) + if mode == "highlight": + original_color = mob.color + mob.set_color(color) + self.wait(frame_time) + mob.set_color(original_color) + if mode == "show_creation": + self.play(ShowCreation(mob, run_time=frame_time)) + if mode == "show": + self.add(mob) + self.wait(frame_time) + if display_numbers: + self.remove(num_mob) + if display_numbers: + self.add(num_mob) + self.number = num_mob + return self + + def count_regions(self, regions, + mode="one_at_a_time", + num_offset=DEFAULT_COUNT_NUM_OFFSET, + run_time=DEFAULT_COUNT_RUN_TIME, + **unused_kwargsn): + if mode not in ["one_at_a_time", "show_all"]: + raise Warning("Unknown mode") + frame_time = run_time / (len(regions)) + for region, count in zip(regions, it.count(1)): + num_mob = Tex(str(count)) + num_mob.center().shift(num_offset) + self.add(num_mob) + self.set_color_region(region) + self.wait(frame_time) + if mode == "one_at_a_time": + self.reset_background() + self.remove(num_mob) + self.add(num_mob) + self.number = num_mob + return self + + +def combinationMobject(n, k): + return Integer(choose(n, k)) + + +class GeneralizedPascalsTriangle(VMobject): + nrows = 7 + height = FRAME_HEIGHT - 1 + width = 1.5 * FRAME_X_RADIUS + portion_to_fill = 0.7 + submob_class = combinationMobject + + def init_points(self): + self.cell_height = float(self.height) / self.nrows + self.cell_width = float(self.width) / self.nrows + self.bottom_left = (self.cell_width * self.nrows / 2.0) * LEFT + \ + (self.cell_height * self.nrows / 2.0) * DOWN + self.coords_to_mobs = {} + self.coords = [ + (n, k) + for n in range(self.nrows) + for k in range(n + 1) + ] + for n, k in self.coords: + center = self.coords_to_center(n, k) + num_mob = self.submob_class(n, k) # Tex(str(num)) + scale_factor = min( + 1, + self.portion_to_fill * self.cell_height / num_mob.get_height(), + self.portion_to_fill * self.cell_width / num_mob.get_width(), + ) + num_mob.center().scale(scale_factor).shift(center) + if n not in self.coords_to_mobs: + self.coords_to_mobs[n] = {} + self.coords_to_mobs[n][k] = num_mob + self.add(*[ + self.coords_to_mobs[n][k] + for n, k in self.coords + ]) + return self + + def coords_to_center(self, n, k): + x_offset = self.cell_width * (k + self.nrows / 2.0 - n / 2.0) + y_offset = self.cell_height * (self.nrows - n) + return self.bottom_left + x_offset * RIGHT + y_offset * UP + + def generate_n_choose_k_mobs(self): + self.coords_to_n_choose_k = {} + for n, k in self.coords: + nck_mob = Tex(r"{%d \choose %d}" % (n, k)) + scale_factor = min( + 1, + self.portion_to_fill * self.cell_height / nck_mob.get_height(), + self.portion_to_fill * self.cell_width / nck_mob.get_width(), + ) + center = self.coords_to_mobs[n][k].get_center() + nck_mob.center().scale(scale_factor).shift(center) + if n not in self.coords_to_n_choose_k: + self.coords_to_n_choose_k[n] = {} + self.coords_to_n_choose_k[n][k] = nck_mob + return self + + def fill_with_n_choose_k(self): + if not hasattr(self, "coords_to_n_choose_k"): + self.generate_n_choose_k_mobs() + self.set_submobjects([]) + self.add(*[ + self.coords_to_n_choose_k[n][k] + for n, k in self.coords + ]) + return self + + def generate_sea_of_zeros(self): + zero = Tex("0") + self.sea_of_zeros = [] + for n in range(self.nrows): + for a in range((self.nrows - n) / 2 + 1): + for k in (n + a + 1, -a - 1): + self.coords.append((n, k)) + mob = zero.copy() + mob.shift(self.coords_to_center(n, k)) + self.coords_to_mobs[n][k] = mob + self.add(mob) + return self + + def get_lowest_row(self): + n = self.nrows - 1 + lowest_row = VGroup(*[ + self.coords_to_mobs[n][k] + for k in range(n + 1) + ]) + return lowest_row + + +class PascalsTriangle(GeneralizedPascalsTriangle): + submob_class = combinationMobject diff --git a/once_useful_constructs/complex_transformation_scene.py b/once_useful_constructs/complex_transformation_scene.py new file mode 100644 index 0000000..b21747d --- /dev/null +++ b/once_useful_constructs/complex_transformation_scene.py @@ -0,0 +1,156 @@ +from manimlib.animation.animation import Animation +from manimlib.animation.movement import ComplexHomotopy +from manimlib.animation.transform import MoveToTarget +from manimlib.constants import * +from manimlib.mobject.coordinate_systems import ComplexPlane +from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.scene.scene import Scene + + +# TODO, refactor this full scene +class ComplexTransformationScene(Scene): + CONFIG = { + "plane_config": {}, + "background_fade_factor": 0.5, + "use_multicolored_plane": False, + "vert_start_color": BLUE, # TODO + "vert_end_color": BLUE, + "horiz_start_color": BLUE, + "horiz_end_color": BLUE, + "num_anchors_to_add_per_line": 50, + "post_transformation_stroke_width": None, + "default_apply_complex_function_kwargs": { + "run_time": 5, + }, + "background_label_scale_val": 0.5, + "include_coordinate_labels": True, + } + + def setup(self): + self.foreground_mobjects = [] + self.transformable_mobjects = [] + self.add_background_plane() + if self.include_coordinate_labels: + self.add_coordinate_labels() + + def add_foreground_mobject(self, mobject): + self.add_foreground_mobjects(mobject) + + def add_transformable_mobjects(self, *mobjects): + self.transformable_mobjects += list(mobjects) + self.add(*mobjects) + + def add_foreground_mobjects(self, *mobjects): + self.foreground_mobjects += list(mobjects) + Scene.add(self, *mobjects) + + def add(self, *mobjects): + Scene.add(self, *list(mobjects) + self.foreground_mobjects) + + def play(self, *animations, **kwargs): + Scene.play( + self, + *list(animations) + list(map(Animation, self.foreground_mobjects)), + **kwargs + ) + + def add_background_plane(self): + background = ComplexPlane(**self.plane_config) + background.fade(self.background_fade_factor) + self.add(background) + self.background = background + + def add_coordinate_labels(self): + self.background.add_coordinates() + self.add(self.background) + + def add_transformable_plane(self, **kwargs): + self.plane = self.get_transformable_plane() + self.add(self.plane) + + def get_transformable_plane(self, x_range=None, y_range=None): + """ + x_range and y_range would be tuples (min, max) + """ + plane_config = dict(self.plane_config) + shift_val = ORIGIN + if x_range is not None: + x_min, x_max = x_range + plane_config["x_radius"] = x_max - x_min + shift_val += (x_max + x_min) * RIGHT / 2. + if y_range is not None: + y_min, y_max = y_range + plane_config["y_radius"] = y_max - y_min + shift_val += (y_max + y_min) * UP / 2. + plane = ComplexPlane(**plane_config) + plane.shift(shift_val) + if self.use_multicolored_plane: + self.paint_plane(plane) + return plane + + def prepare_for_transformation(self, mob): + if hasattr(mob, "prepare_for_nonlinear_transform"): + mob.prepare_for_nonlinear_transform( + self.num_anchors_to_add_per_line + ) + # TODO... + + def paint_plane(self, plane): + for lines in planes, plane.secondary_lines: + lines.set_color_by_gradient( + self.vert_start_color, + self.vert_end_color, + self.horiz_start_color, + self.horiz_end_color, + ) + # plane.axes.set_color_by_gradient( + # self.horiz_start_color, + # self.vert_start_color + # ) + + def z_to_point(self, z): + return self.background.number_to_point(z) + + def get_transformer(self, **kwargs): + transform_kwargs = dict(self.default_apply_complex_function_kwargs) + transform_kwargs.update(kwargs) + transformer = VGroup() + if hasattr(self, "plane"): + self.prepare_for_transformation(self.plane) + transformer.add(self.plane) + transformer.add(*self.transformable_mobjects) + return transformer, transform_kwargs + + def apply_complex_function(self, func, added_anims=[], **kwargs): + transformer, transform_kwargs = self.get_transformer(**kwargs) + transformer.generate_target() + # Rescale, apply function, scale back + transformer.target.shift(-self.background.get_center_point()) + transformer.target.scale(1. / self.background.unit_size) + transformer.target.apply_complex_function(func) + transformer.target.scale(self.background.unit_size) + transformer.target.shift(self.background.get_center_point()) + # + + for mob in transformer.target[0].family_members_with_points(): + mob.make_smooth() + if self.post_transformation_stroke_width is not None: + transformer.target.set_stroke( + width=self.post_transformation_stroke_width) + self.play( + MoveToTarget(transformer, **transform_kwargs), + *added_anims + ) + + def apply_complex_homotopy(self, complex_homotopy, added_anims=[], **kwargs): + transformer, transform_kwargs = self.get_transformer(**kwargs) + + # def homotopy(x, y, z, t): + # output = complex_homotopy(complex(x, y), t) + # rescaled_output = self.z_to_point(output) + # return (rescaled_output.real, rescaled_output.imag, z) + + self.play( + ComplexHomotopy(complex_homotopy, transformer, **transform_kwargs), + *added_anims + ) diff --git a/once_useful_constructs/counting.py b/once_useful_constructs/counting.py new file mode 100644 index 0000000..ad54da2 --- /dev/null +++ b/once_useful_constructs/counting.py @@ -0,0 +1,263 @@ +from manimlib.animation.creation import ShowCreation +from manimlib.animation.fading import FadeIn +from manimlib.animation.transform import MoveToTarget +from manimlib.animation.transform import Transform +from manimlib.constants import * +from manimlib.mobject.geometry import Arrow +from manimlib.mobject.geometry import Circle +from manimlib.mobject.geometry import Dot +from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.scene.scene import Scene + +import itertools as it + +class CountingScene(Scene): + CONFIG = { + "digit_place_colors": [YELLOW, MAROON_B, RED, GREEN, BLUE, PURPLE_D], + "counting_dot_starting_position": (FRAME_X_RADIUS - 1) * RIGHT + (FRAME_Y_RADIUS - 1) * UP, + "count_dot_starting_radius": 0.5, + "dot_configuration_height": 2, + "ones_configuration_location": UP + 2 * RIGHT, + "num_scale_factor": 2, + "num_start_location": 2 * DOWN, + } + + def setup(self): + self.dots = VGroup() + self.number = 0 + self.max_place = 0 + self.number_mob = VGroup(Tex(str(self.number))) + self.number_mob.scale(self.num_scale_factor) + self.number_mob.shift(self.num_start_location) + + self.dot_templates = [] + self.dot_template_iterators = [] + self.curr_configurations = [] + + self.arrows = VGroup() + + self.add(self.number_mob) + + def get_template_configuration(self, place): + # This should probably be replaced for non-base-10 counting scenes + down_right = (0.5) * RIGHT + (np.sqrt(3) / 2) * DOWN + result = [] + for down_right_steps in range(5): + for left_steps in range(down_right_steps): + result.append( + down_right_steps * down_right + left_steps * LEFT + ) + return reversed(result[:self.get_place_max(place)]) + + def get_dot_template(self, place): + # This should be replaced for non-base-10 counting scenes + dots = VGroup(*[ + Dot( + point, + radius=0.25, + fill_opacity=0, + stroke_width=2, + stroke_color=WHITE, + ) + for point in self.get_template_configuration(place) + ]) + dots.set_height(self.dot_configuration_height) + return dots + + def add_configuration(self): + new_template = self.get_dot_template(len(self.dot_templates)) + new_template.move_to(self.ones_configuration_location) + left_vect = (new_template.get_width() + LARGE_BUFF) * LEFT + new_template.shift( + left_vect * len(self.dot_templates) + ) + self.dot_templates.append(new_template) + self.dot_template_iterators.append( + it.cycle(new_template) + ) + self.curr_configurations.append(VGroup()) + + def count(self, max_val, run_time_per_anim=1): + for x in range(max_val): + self.increment(run_time_per_anim) + + def increment(self, run_time_per_anim=1): + moving_dot = Dot( + self.counting_dot_starting_position, + radius=self.count_dot_starting_radius, + color=self.digit_place_colors[0], + ) + moving_dot.generate_target() + moving_dot.set_fill(opacity=0) + kwargs = { + "run_time": run_time_per_anim + } + + continue_rolling_over = True + first_move = True + place = 0 + while continue_rolling_over: + added_anims = [] + if first_move: + added_anims += self.get_digit_increment_animations() + first_move = False + moving_dot.target.replace( + next(self.dot_template_iterators[place]) + ) + self.play(MoveToTarget(moving_dot), *added_anims, **kwargs) + self.curr_configurations[place].add(moving_dot) + + if len(self.curr_configurations[place].split()) == self.get_place_max(place): + full_configuration = self.curr_configurations[place] + self.curr_configurations[place] = VGroup() + place += 1 + center = full_configuration.get_center_of_mass() + radius = 0.6 * max( + full_configuration.get_width(), + full_configuration.get_height(), + ) + circle = Circle( + radius=radius, + stroke_width=0, + fill_color=self.digit_place_colors[place], + fill_opacity=0.5, + ) + circle.move_to(center) + moving_dot = VGroup(circle, full_configuration) + moving_dot.generate_target() + moving_dot[0].set_fill(opacity=0) + else: + continue_rolling_over = False + + def get_digit_increment_animations(self): + result = [] + self.number += 1 + is_next_digit = self.is_next_digit() + if is_next_digit: + self.max_place += 1 + new_number_mob = self.get_number_mob(self.number) + new_number_mob.move_to(self.number_mob, RIGHT) + if is_next_digit: + self.add_configuration() + place = len(new_number_mob.split()) - 1 + result.append(FadeIn(self.dot_templates[place])) + arrow = Arrow( + new_number_mob[place].get_top(), + self.dot_templates[place].get_bottom(), + color=self.digit_place_colors[place] + ) + self.arrows.add(arrow) + result.append(ShowCreation(arrow)) + result.append(Transform( + self.number_mob, new_number_mob, + lag_ratio=0.5 + )) + return result + + def get_number_mob(self, num): + result = VGroup() + place = 0 + max_place = self.max_place + while place < max_place: + digit = Tex(str(self.get_place_num(num, place))) + if place >= len(self.digit_place_colors): + self.digit_place_colors += self.digit_place_colors + digit.set_color(self.digit_place_colors[place]) + digit.scale(self.num_scale_factor) + digit.next_to(result, LEFT, buff=SMALL_BUFF, aligned_edge=DOWN) + result.add(digit) + place += 1 + return result + + def is_next_digit(self): + return False + + def get_place_num(self, num, place): + return 0 + + def get_place_max(self, place): + return 0 + + +class PowerCounter(CountingScene): + def is_next_digit(self): + number = self.number + while number > 1: + if number % self.base != 0: + return False + number /= self.base + return True + + def get_place_max(self, place): + return self.base + + def get_place_num(self, num, place): + return (num / (self.base ** place)) % self.base + + +class CountInDecimal(PowerCounter): + CONFIG = { + "base": 10, + } + + def construct(self): + for x in range(11): + self.increment() + for x in range(85): + self.increment(0.25) + for x in range(20): + self.increment() + + +class CountInTernary(PowerCounter): + CONFIG = { + "base": 3, + "dot_configuration_height": 1, + "ones_configuration_location": UP + 4 * RIGHT + } + + def construct(self): + self.count(27) + + # def get_template_configuration(self, place): + # return [ORIGIN, UP] + + +class CountInBinaryTo256(PowerCounter): + CONFIG = { + "base": 2, + "dot_configuration_height": 1, + "ones_configuration_location": UP + 5 * RIGHT + } + + def construct(self): + self.count(128, 0.3) + + def get_template_configuration(self, place): + return [ORIGIN, UP] + + +class FactorialBase(CountingScene): + CONFIG = { + "dot_configuration_height": 1, + "ones_configuration_location": UP + 4 * RIGHT + } + + def construct(self): + self.count(30, 0.4) + + def is_next_digit(self): + return self.number == self.factorial(self.max_place + 1) + + def get_place_max(self, place): + return place + 2 + + def get_place_num(self, num, place): + return (num / self.factorial(place + 1)) % self.get_place_max(place) + + def factorial(self, n): + if (n == 1): + return 1 + else: + return n * self.factorial(n - 1) diff --git a/once_useful_constructs/dict_shenanigans.py b/once_useful_constructs/dict_shenanigans.py new file mode 100644 index 0000000..4b08d29 --- /dev/null +++ b/once_useful_constructs/dict_shenanigans.py @@ -0,0 +1,67 @@ +import inspect +from manimlib.utils.dict_ops import merge_dicts_recursively + + +def filtered_locals(caller_locals): + result = caller_locals.copy() + ignored_local_args = ["self", "kwargs"] + for arg in ignored_local_args: + result.pop(arg, caller_locals) + return result + + +def digest_config(obj, kwargs, caller_locals={}): + """ + (Deprecated) + + Sets init args and CONFIG values as local variables + + The purpose of this function is to ensure that all + configuration of any object is inheritable, able to + be easily passed into instantiation, and is attached + as an attribute of the object. + """ + + # Assemble list of CONFIGs from all super classes + classes_in_hierarchy = [obj.__class__] + static_configs = [] + while len(classes_in_hierarchy) > 0: + Class = classes_in_hierarchy.pop() + classes_in_hierarchy += Class.__bases__ + if hasattr(Class, "CONFIG"): + static_configs.append(Class.CONFIG) + + # Order matters a lot here, first dicts have higher priority + caller_locals = filtered_locals(caller_locals) + all_dicts = [kwargs, caller_locals, obj.__dict__] + all_dicts += static_configs + obj.__dict__ = merge_dicts_recursively(*reversed(all_dicts)) + + + +def digest_locals(obj, keys=None): + caller_locals = filtered_locals( + inspect.currentframe().f_back.f_locals + ) + if keys is None: + keys = list(caller_locals.keys()) + for key in keys: + setattr(obj, key, caller_locals[key]) + + +# Occasionally convenient in order to write dict.x instead of more laborious +# (and less in keeping with all other attr accesses) dict["x"] + +class DictAsObject(object): + def __init__(self, dict): + self.__dict__ = dict + + +def get_all_descendent_classes(Class): + awaiting_review = [Class] + result = [] + while awaiting_review: + Child = awaiting_review.pop() + awaiting_review += Child.__subclasses__() + result.append(Child) + return result diff --git a/once_useful_constructs/fractals.py b/once_useful_constructs/fractals.py new file mode 100644 index 0000000..4dcffb8 --- /dev/null +++ b/once_useful_constructs/fractals.py @@ -0,0 +1,628 @@ +from functools import reduce +import random + +from manimlib.constants import * +# from manimlib.for_3b1b_videos.pi_creature import PiCreature +# from manimlib.for_3b1b_videos.pi_creature import Randolph +# from manimlib.for_3b1b_videos.pi_creature import get_all_pi_creature_modes +from manimlib.mobject.geometry import Circle +from manimlib.mobject.geometry import Polygon +from manimlib.mobject.geometry import RegularPolygon +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.color import color_gradient +from manimlib.utils.dict_ops import digest_config +from manimlib.utils.space_ops import center_of_mass +from manimlib.utils.space_ops import compass_directions +from manimlib.utils.space_ops import rotate_vector +from manimlib.utils.space_ops import rotation_matrix + + +def rotate(points, angle=np.pi, axis=OUT): + if axis is None: + return points + matrix = rotation_matrix(angle, axis) + points = np.dot(points, np.transpose(matrix)) + return points + + +def fractalify(vmobject, order=3, *args, **kwargs): + for x in range(order): + fractalification_iteration(vmobject) + return vmobject + + +def fractalification_iteration(vmobject, dimension=1.05, num_inserted_anchors_range=list(range(1, 4))): + num_points = vmobject.get_num_points() + if num_points > 0: + # original_anchors = vmobject.get_anchors() + original_anchors = [ + vmobject.point_from_proportion(x) + for x in np.linspace(0, 1 - 1. / num_points, num_points) + ] + new_anchors = [] + for p1, p2, in zip(original_anchors, original_anchors[1:]): + num_inserts = random.choice(num_inserted_anchors_range) + inserted_points = [ + interpolate(p1, p2, alpha) + for alpha in np.linspace(0, 1, num_inserts + 2)[1:-1] + ] + mass_scaling_factor = 1. / (num_inserts + 1) + length_scaling_factor = mass_scaling_factor**(1. / dimension) + target_length = get_norm(p1 - p2) * length_scaling_factor + curr_length = get_norm(p1 - p2) * mass_scaling_factor + # offset^2 + curr_length^2 = target_length^2 + offset_len = np.sqrt(target_length**2 - curr_length**2) + unit_vect = (p1 - p2) / get_norm(p1 - p2) + offset_unit_vect = rotate_vector(unit_vect, np.pi / 2) + inserted_points = [ + point + u * offset_len * offset_unit_vect + for u, point in zip(it.cycle([-1, 1]), inserted_points) + ] + new_anchors += [p1] + inserted_points + new_anchors.append(original_anchors[-1]) + vmobject.set_points_as_corners(new_anchors) + vmobject.set_submobjects([ + fractalification_iteration( + submob, dimension, num_inserted_anchors_range) + for submob in vmobject.submobjects + ]) + return vmobject + + +class SelfSimilarFractal(VMobject): + order = 5 + num_subparts = 3 + height = 4 + colors = [RED, WHITE] + stroke_width = 1 # Not the right way to assign stroke width + fill_opacity = 1 # Not the right way to assign fill opacity + + def init_colors(self): + VMobject.init_colors(self) + self.set_color_by_gradient(*self.colors) + + def init_points(self): + order_n_self = self.get_order_n_self(self.order) + if self.order == 0: + self.set_submobjects([order_n_self]) + else: + self.set_submobjects(order_n_self.submobjects) + return self + + def get_order_n_self(self, order): + if order == 0: + result = self.get_seed_shape() + else: + lower_order = self.get_order_n_self(order - 1) + subparts = [ + lower_order.copy() + for x in range(self.num_subparts) + ] + self.arrange_subparts(*subparts) + result = VGroup(*subparts) + + result.set_height(self.height) + result.center() + return result + + def get_seed_shape(self): + raise Exception("Not implemented") + + def arrange_subparts(self, *subparts): + raise Exception("Not implemented") + + +class Sierpinski(SelfSimilarFractal): + def get_seed_shape(self): + return Polygon( + RIGHT, np.sqrt(3) * UP, LEFT, + ) + + def arrange_subparts(self, *subparts): + tri1, tri2, tri3 = subparts + tri1.move_to(tri2.get_corner(DOWN + LEFT), UP) + tri3.move_to(tri2.get_corner(DOWN + RIGHT), UP) + + +class DiamondFractal(SelfSimilarFractal): + num_subparts = 4 + height = 4 + colors = [GREEN_E, YELLOW] + + def get_seed_shape(self): + return RegularPolygon(n=4) + + def arrange_subparts(self, *subparts): + # VGroup(*subparts).rotate(np.pi/4) + for part, vect in zip(subparts, compass_directions(start_vect=UP + RIGHT)): + part.next_to(ORIGIN, vect, buff=0) + VGroup(*subparts).rotate(np.pi / 4, about_point=ORIGIN) + + +class PentagonalFractal(SelfSimilarFractal): + num_subparts = 5 + colors = [MAROON_B, YELLOW, RED] + height = 6 + + def get_seed_shape(self): + return RegularPolygon(n=5, start_angle=np.pi / 2) + + def arrange_subparts(self, *subparts): + for x, part in enumerate(subparts): + part.shift(0.95 * part.get_height() * UP) + part.rotate(2 * np.pi * x / 5, about_point=ORIGIN) + + +class PentagonalPiCreatureFractal(PentagonalFractal): + def init_colors(self): + SelfSimilarFractal.init_colors(self) + internal_pis = [ + pi + for pi in self.get_family() + if isinstance(pi, PiCreature) + ] + colors = color_gradient(self.colors, len(internal_pis)) + for pi, color in zip(internal_pis, colors): + pi.init_colors() + pi.body.set_stroke(color, width=0.5) + pi.set_color(color) + + def get_seed_shape(self): + return Randolph(mode="shruggie") + + def arrange_subparts(self, *subparts): + for part in subparts: + part.rotate(2 * np.pi / 5, about_point=ORIGIN) + PentagonalFractal.arrange_subparts(self, *subparts) + + +class PiCreatureFractal(VMobject): + order = 7 + scale_val = 2.5 + start_mode = "hooray" + height = 6 + colors = [ + BLUE_D, BLUE_B, MAROON_B, MAROON_D, GREY, + YELLOW, RED, GREY_BROWN, RED, RED_E, + ] + random_seed = 0 + stroke_width = 0 + + def init_colors(self): + VMobject.init_colors(self) + internal_pis = [ + pi + for pi in self.get_family() + if isinstance(pi, PiCreature) + ] + random.seed(self.random_seed) + for pi in reversed(internal_pis): + color = random.choice(self.colors) + pi.set_color(color) + pi.set_stroke(color, width=0) + + def init_points(self): + random.seed(self.random_seed) + modes = get_all_pi_creature_modes() + seed = PiCreature(mode=self.start_mode) + seed.set_height(self.height) + seed.to_edge(DOWN) + creatures = [seed] + self.add(VGroup(seed)) + for x in range(self.order): + new_creatures = [] + for creature in creatures: + for eye, vect in zip(creature.eyes, [LEFT, RIGHT]): + new_creature = PiCreature( + mode=random.choice(modes) + ) + new_creature.set_height( + self.scale_val * eye.get_height() + ) + new_creature.next_to( + eye, vect, + buff=0, + aligned_edge=DOWN + ) + new_creatures.append(new_creature) + creature.look_at(random.choice(new_creatures)) + self.add_to_back(VGroup(*new_creatures)) + creatures = new_creatures + + # def init_colors(self): + # VMobject.init_colors(self) + # self.set_color_by_gradient(*self.colors) + + +class WonkyHexagonFractal(SelfSimilarFractal): + num_subparts = 7 + + def get_seed_shape(self): + return RegularPolygon(n=6) + + def arrange_subparts(self, *subparts): + for i, piece in enumerate(subparts): + piece.rotate(i * np.pi / 12, about_point=ORIGIN) + p1, p2, p3, p4, p5, p6, p7 = subparts + center_row = VGroup(p1, p4, p7) + center_row.arrange(RIGHT, buff=0) + for p in p2, p3, p5, p6: + p.set_width(p1.get_width()) + p2.move_to(p1.get_top(), DOWN + LEFT) + p3.move_to(p1.get_bottom(), UP + LEFT) + p5.move_to(p4.get_top(), DOWN + LEFT) + p6.move_to(p4.get_bottom(), UP + LEFT) + + +class CircularFractal(SelfSimilarFractal): + num_subparts = 3 + colors = [GREEN, BLUE, GREY] + + def get_seed_shape(self): + return Circle() + + def arrange_subparts(self, *subparts): + if not hasattr(self, "been_here"): + self.num_subparts = 3 + self.order + self.been_here = True + for i, part in enumerate(subparts): + theta = np.pi / self.num_subparts + part.next_to( + ORIGIN, UP, + buff=self.height / (2 * np.tan(theta)) + ) + part.rotate(i * 2 * np.pi / self.num_subparts, about_point=ORIGIN) + self.num_subparts -= 1 + +######## Space filling curves ############ + + +class JaggedCurvePiece(VMobject): + def insert_n_curves(self, n): + if self.get_num_curves() == 0: + self.set_points(np.zeros((1, 3))) + anchors = self.get_anchors() + indices = np.linspace( + 0, len(anchors) - 1, n + len(anchors) + ).astype('int') + self.set_points_as_corners(anchors[indices]) + + +class FractalCurve(VMobject): + radius = 3 + order = 5 + colors = [RED, GREEN] + num_submobjects = 20 + monochromatic = False + order_to_stroke_width_map = { + 3: 3, + 4: 2, + 5: 1, + } + + def init_points(self): + points = self.get_anchor_points() + self.set_points_as_corners(points) + if not self.monochromatic: + alphas = np.linspace(0, 1, self.num_submobjects) + for alpha_pair in zip(alphas, alphas[1:]): + submobject = JaggedCurvePiece() + submobject.pointwise_become_partial( + self, *alpha_pair + ) + self.add(submobject) + self.set_points(np.zeros((0, 3))) + + def init_colors(self): + VMobject.init_colors(self) + self.set_color_by_gradient(*self.colors) + for order in sorted(self.order_to_stroke_width_map.keys()): + if self.order >= order: + self.set_stroke(width=self.order_to_stroke_width_map[order]) + + def get_anchor_points(self): + raise Exception("Not implemented") + + +class LindenmayerCurve(FractalCurve): + axiom = "A" + rule = {} + scale_factor = 2 + radius = 3 + start_step = RIGHT + angle = np.pi / 2 + + def expand_command_string(self, command): + result = "" + for letter in command: + if letter in self.rule: + result += self.rule[letter] + else: + result += letter + return result + + def get_command_string(self): + result = self.axiom + for x in range(self.order): + result = self.expand_command_string(result) + return result + + def get_anchor_points(self): + step = float(self.radius) * self.start_step + step /= (self.scale_factor**self.order) + curr = np.zeros(3) + result = [curr] + for letter in self.get_command_string(): + if letter == "+": + step = rotate(step, self.angle) + elif letter == "-": + step = rotate(step, -self.angle) + else: + curr = curr + step + result.append(curr) + return np.array(result) - center_of_mass(result) + + +class SelfSimilarSpaceFillingCurve(FractalCurve): + offsets = [] + # keys must awkwardly be in string form... + offset_to_rotation_axis = {} + scale_factor = 2 + radius_scale_factor = 0.5 + + def transform(self, points, offset): + """ + How to transform the copy of points shifted by + offset. Generally meant to be extended in subclasses + """ + copy = np.array(points) + if str(offset) in self.offset_to_rotation_axis: + copy = rotate( + copy, + axis=self.offset_to_rotation_axis[str(offset)] + ) + copy /= self.scale_factor, + copy += offset * self.radius * self.radius_scale_factor + return copy + + def refine_into_subparts(self, points): + transformed_copies = [ + self.transform(points, offset) + for offset in self.offsets + ] + return reduce( + lambda a, b: np.append(a, b, axis=0), + transformed_copies + ) + + def get_anchor_points(self): + points = np.zeros((1, 3)) + for count in range(self.order): + points = self.refine_into_subparts(points) + return points + + def generate_grid(self): + raise Exception("Not implemented") + + +class HilbertCurve(SelfSimilarSpaceFillingCurve): + offsets = [ + LEFT + DOWN, + LEFT + UP, + RIGHT + UP, + RIGHT + DOWN, + ] + offset_to_rotation_axis = { + str(LEFT + DOWN): RIGHT + UP, + str(RIGHT + DOWN): RIGHT + DOWN, + } + + +class HilbertCurve3D(SelfSimilarSpaceFillingCurve): + offsets = [ + RIGHT + DOWN + IN, + LEFT + DOWN + IN, + LEFT + DOWN + OUT, + RIGHT + DOWN + OUT, + RIGHT + UP + OUT, + LEFT + UP + OUT, + LEFT + UP + IN, + RIGHT + UP + IN, + ] + offset_to_rotation_axis_and_angle = { + str(RIGHT + DOWN + IN): (LEFT + UP + OUT, 2 * np.pi / 3), + str(LEFT + DOWN + IN): (RIGHT + DOWN + IN, 2 * np.pi / 3), + str(LEFT + DOWN + OUT): (RIGHT + DOWN + IN, 2 * np.pi / 3), + str(RIGHT + DOWN + OUT): (UP, np.pi), + str(RIGHT + UP + OUT): (UP, np.pi), + str(LEFT + UP + OUT): (LEFT + DOWN + OUT, 2 * np.pi / 3), + str(LEFT + UP + IN): (LEFT + DOWN + OUT, 2 * np.pi / 3), + str(RIGHT + UP + IN): (RIGHT + UP + IN, 2 * np.pi / 3), + } + # Rewrote transform method to include the rotation angle + + def transform(self, points, offset): + copy = np.array(points) + copy = rotate( + copy, + axis=self.offset_to_rotation_axis_and_angle[str(offset)][0], + angle=self.offset_to_rotation_axis_and_angle[str(offset)][1], + ) + copy /= self.scale_factor, + copy += offset * self.radius * self.radius_scale_factor + return copy + + +class PeanoCurve(SelfSimilarSpaceFillingCurve): + colors = [PURPLE, TEAL] + offsets = [ + LEFT + DOWN, + LEFT, + LEFT + UP, + UP, + ORIGIN, + DOWN, + RIGHT + DOWN, + RIGHT, + RIGHT + UP, + ] + offset_to_rotation_axis = { + str(LEFT): UP, + str(UP): RIGHT, + str(ORIGIN): LEFT + UP, + str(DOWN): RIGHT, + str(RIGHT): UP, + } + scale_factor = 3 + radius_scale_factor = 2.0 / 3 + + +class TriangleFillingCurve(SelfSimilarSpaceFillingCurve): + colors = [MAROON, YELLOW] + offsets = [ + LEFT / 4. + DOWN / 6., + ORIGIN, + RIGHT / 4. + DOWN / 6., + UP / 3., + ] + offset_to_rotation_axis = { + str(ORIGIN): RIGHT, + str(UP / 3.): UP, + } + scale_factor = 2 + radius_scale_factor = 1.5 + + +class HexagonFillingCurve(SelfSimilarSpaceFillingCurve): + start_color = WHITE + end_color = BLUE_D + axis_offset_pairs = [ + (None, 1.5*DOWN + 0.5*np.sqrt(3)*LEFT), + (UP+np.sqrt(3)*RIGHT, 1.5*DOWN + 0.5*np.sqrt(3)*RIGHT), + (np.sqrt(3)*UP+RIGHT, ORIGIN), + ((UP, RIGHT), np.sqrt(3)*LEFT), + (None, 1.5*UP + 0.5*np.sqrt(3)*LEFT), + (None, 1.5*UP + 0.5*np.sqrt(3)*RIGHT), + (RIGHT, np.sqrt(3)*RIGHT), + ] + scale_factor = 3 + radius_scale_factor = 2/(3*np.sqrt(3)) + + def refine_into_subparts(self, points): + return SelfSimilarSpaceFillingCurve.refine_into_subparts( + self, + rotate(points, np.pi/6, IN) + ) + + +class UtahFillingCurve(SelfSimilarSpaceFillingCurve): + colors = [WHITE, BLUE_D] + axis_offset_pairs = [] + scale_factor = 3 + radius_scale_factor = 2 / (3 * np.sqrt(3)) + + +class FlowSnake(LindenmayerCurve): + colors = [YELLOW, GREEN] + axiom = "A" + rule = { + "A": "A-B--B+A++AA+B-", + "B": "+A-BB--B-A++A+B", + } + radius = 6 # TODO, this is innaccurate + scale_factor = np.sqrt(7) + start_step = RIGHT + angle = -np.pi / 3 + + def __init__(self, **kwargs): + LindenmayerCurve.__init__(self, **kwargs) + self.rotate(-self.order * np.pi / 9, about_point=ORIGIN) + + +class SierpinskiCurve(LindenmayerCurve): + colors = [RED, WHITE] + axiom = "B" + rule = { + "A": "+B-A-B+", + "B": "-A+B+A-", + } + radius = 6 # TODO, this is innaccurate + scale_factor = 2 + start_step = RIGHT + angle = -np.pi / 3 + + +class KochSnowFlake(LindenmayerCurve): + colors = [BLUE_D, WHITE, BLUE_D] + axiom = "A--A--A--" + rule = { + "A": "A+A--A+A" + } + radius = 4 + scale_factor = 3 + start_step = RIGHT + angle = np.pi / 3 + order_to_stroke_width_map = { + 3: 3, + 5: 2, + 6: 1, + } + + def __init__(self, **kwargs): + digest_config(self, kwargs) + self.scale_factor = 2 * (1 + np.cos(self.angle)) + LindenmayerCurve.__init__(self, **kwargs) + + +class KochCurve(KochSnowFlake): + axiom = "A--" + + +class QuadraticKoch(LindenmayerCurve): + colors = [YELLOW, WHITE, MAROON_B] + axiom = "A" + rule = {"A": "A+A-A-AA+A+A-A"} + radius = 4 + scale_factor = 4 + start_step = RIGHT + angle = np.pi / 2 + + +class QuadraticKochIsland(QuadraticKoch): + axiom = "A+A+A+A" + + +class StellarCurve(LindenmayerCurve): + start_color = RED + end_color = BLUE_E + rule = { + "A": "+B-A-B+A-B+", + "B": "-A+B+A-B+A-", + } + scale_factor = 3 + angle = 2 * np.pi / 5 + + +class SnakeCurve(FractalCurve): + start_color = BLUE + end_color = YELLOW + + def get_anchor_points(self): + result = [] + resolution = 2**self.order + step = 2.0 * self.radius / resolution + lower_left = ORIGIN + \ + LEFT * (self.radius - step / 2) + \ + DOWN * (self.radius - step / 2) + + for y in range(resolution): + x_range = list(range(resolution)) + if y % 2 == 0: + x_range.reverse() + for x in x_range: + result.append( + lower_left + x * step * RIGHT + y * step * UP + ) + return result diff --git a/once_useful_constructs/graph_scene.py b/once_useful_constructs/graph_scene.py new file mode 100644 index 0000000..f59f623 --- /dev/null +++ b/once_useful_constructs/graph_scene.py @@ -0,0 +1,564 @@ +import itertools as it + +from manimlib.animation.creation import Write, DrawBorderThenFill, ShowCreation +from manimlib.animation.transform import Transform +from manimlib.animation.update import UpdateFromAlphaFunc +from manimlib.constants import * +from manimlib.mobject.functions import ParametricCurve +from manimlib.mobject.geometry import Line +from manimlib.mobject.geometry import Rectangle +from manimlib.mobject.geometry import RegularPolygon +from manimlib.mobject.number_line import NumberLine +from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.mobject.svg.tex_mobject import TexText +from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.mobject.types.vectorized_mobject import VectorizedPoint +from manimlib.scene.scene import Scene +from manimlib.utils.bezier import interpolate +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 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): + x_min = -1 + x_max = 10 + x_axis_width = 9 + x_tick_frequency = 1 + x_leftmost_tick = None # Change if different from x_min + x_labeled_nums = None + x_axis_label = "$x$" + y_min = -1 + y_max = 10 + y_axis_height = 6 + y_tick_frequency = 1 + y_bottom_tick = None # Change if different from y_min + y_labeled_nums = None + y_axis_label = "$y$" + axes_color = GREY + graph_origin = 2.5 * DOWN + 4 * LEFT + exclude_zero_label = True + default_graph_colors = [BLUE, GREEN, YELLOW] + default_derivative_color = GREEN + default_input_color = YELLOW + default_riemann_start_color = BLUE + default_riemann_end_color = GREEN + area_opacity = 0.8 + num_rects = 50 + + def setup(self): + self.default_graph_colors_cycle = it.cycle(self.default_graph_colors) + + self.left_T_label = VGroup() + self.left_v_line = VGroup() + self.right_T_label = VGroup() + self.right_v_line = VGroup() + + def setup_axes(self, animate=False): + # TODO, once eoc is done, refactor this to be less redundant. + x_num_range = float(self.x_max - self.x_min) + self.space_unit_to_x = self.x_axis_width / x_num_range + if self.x_labeled_nums is None: + self.x_labeled_nums = [] + if self.x_leftmost_tick is None: + self.x_leftmost_tick = self.x_min + x_axis = NumberLine( + x_min=self.x_min, + x_max=self.x_max, + unit_size=self.space_unit_to_x, + tick_frequency=self.x_tick_frequency, + leftmost_tick=self.x_leftmost_tick, + numbers_with_elongated_ticks=self.x_labeled_nums, + color=self.axes_color + ) + x_axis.shift(self.graph_origin - x_axis.number_to_point(0)) + if len(self.x_labeled_nums) > 0: + if self.exclude_zero_label: + self.x_labeled_nums = [x for x in self.x_labeled_nums if x != 0] + x_axis.add_numbers(self.x_labeled_nums) + if self.x_axis_label: + x_label = TexText(self.x_axis_label) + x_label.next_to( + x_axis.get_tick_marks(), UP + RIGHT, + buff=SMALL_BUFF + ) + x_label.shift_onto_screen() + x_axis.add(x_label) + self.x_axis_label_mob = x_label + + y_num_range = float(self.y_max - self.y_min) + self.space_unit_to_y = self.y_axis_height / y_num_range + + if self.y_labeled_nums is None: + self.y_labeled_nums = [] + if self.y_bottom_tick is None: + self.y_bottom_tick = self.y_min + y_axis = NumberLine( + x_min=self.y_min, + x_max=self.y_max, + unit_size=self.space_unit_to_y, + tick_frequency=self.y_tick_frequency, + leftmost_tick=self.y_bottom_tick, + numbers_with_elongated_ticks=self.y_labeled_nums, + color=self.axes_color, + line_to_number_vect=LEFT, + label_direction=LEFT, + ) + y_axis.shift(self.graph_origin - y_axis.number_to_point(0)) + y_axis.rotate(np.pi / 2, about_point=y_axis.number_to_point(0)) + if len(self.y_labeled_nums) > 0: + if self.exclude_zero_label: + self.y_labeled_nums = [y for y in self.y_labeled_nums if y != 0] + y_axis.add_numbers(self.y_labeled_nums) + if self.y_axis_label: + y_label = TexText(self.y_axis_label) + y_label.next_to( + y_axis.get_corner(UP + RIGHT), UP + RIGHT, + buff=SMALL_BUFF + ) + y_label.shift_onto_screen() + y_axis.add(y_label) + self.y_axis_label_mob = y_label + + if animate: + self.play(Write(VGroup(x_axis, y_axis))) + else: + self.add(x_axis, y_axis) + self.x_axis, self.y_axis = self.axes = VGroup(x_axis, y_axis) + self.default_graph_colors = it.cycle(self.default_graph_colors) + + def coords_to_point(self, x, y): + assert(hasattr(self, "x_axis") and hasattr(self, "y_axis")) + result = self.x_axis.number_to_point(x)[0] * RIGHT + result += self.y_axis.number_to_point(y)[1] * UP + return result + + def point_to_coords(self, point): + return (self.x_axis.point_to_number(point), + self.y_axis.point_to_number(point)) + + def get_graph( + self, func, + color=None, + x_min=None, + x_max=None, + **kwargs + ): + if color is None: + color = next(self.default_graph_colors_cycle) + if x_min is None: + x_min = self.x_min + if x_max is None: + x_max = self.x_max + + def parameterized_function(alpha): + x = interpolate(x_min, x_max, alpha) + y = func(x) + if not np.isfinite(y): + y = self.y_max + return self.coords_to_point(x, y) + + graph = ParametricCurve( + parameterized_function, + color=color, + **kwargs + ) + graph.underlying_function = func + return graph + + def input_to_graph_point(self, x, graph): + return self.coords_to_point(x, graph.underlying_function(x)) + + def angle_of_tangent(self, x, graph, dx=0.01): + vect = self.input_to_graph_point( + x + dx, graph) - self.input_to_graph_point(x, graph) + return angle_of_vector(vect) + + def slope_of_tangent(self, *args, **kwargs): + return np.tan(self.angle_of_tangent(*args, **kwargs)) + + def get_derivative_graph(self, graph, dx=0.01, **kwargs): + if "color" not in kwargs: + kwargs["color"] = self.default_derivative_color + + def deriv(x): + return self.slope_of_tangent(x, graph, dx) / self.space_unit_to_y + return self.get_graph(deriv, **kwargs) + + def get_graph_label( + self, + graph, + label="f(x)", + x_val=None, + direction=RIGHT, + buff=MED_SMALL_BUFF, + color=None, + ): + label = Tex(label) + color = color or graph.get_color() + label.set_color(color) + if x_val is None: + # Search from right to left + for x in np.linspace(self.x_max, self.x_min, 100): + point = self.input_to_graph_point(x, graph) + if point[1] < FRAME_Y_RADIUS: + break + x_val = x + label.next_to( + self.input_to_graph_point(x_val, graph), + direction, + buff=buff + ) + label.shift_onto_screen() + return label + + def get_riemann_rectangles( + self, + graph, + x_min=None, + x_max=None, + dx=0.1, + input_sample_type="left", + stroke_width=1, + stroke_color=BLACK, + fill_opacity=1, + start_color=None, + end_color=None, + show_signed_area=True, + width_scale_factor=1.001 + ): + x_min = x_min if x_min is not None else self.x_min + x_max = x_max if x_max is not None else self.x_max + if start_color is None: + start_color = self.default_riemann_start_color + if end_color is None: + end_color = self.default_riemann_end_color + rectangles = VGroup() + x_range = np.arange(x_min, x_max, dx) + colors = color_gradient([start_color, end_color], len(x_range)) + for x, color in zip(x_range, colors): + if input_sample_type == "left": + sample_input = x + elif input_sample_type == "right": + sample_input = x + dx + elif input_sample_type == "center": + sample_input = x + 0.5 * dx + else: + raise Exception("Invalid input sample type") + graph_point = self.input_to_graph_point(sample_input, graph) + points = VGroup(*list(map(VectorizedPoint, [ + self.coords_to_point(x, 0), + self.coords_to_point(x + width_scale_factor * dx, 0), + graph_point + ]))) + + rect = Rectangle() + rect.replace(points, stretch=True) + if graph_point[1] < self.graph_origin[1] and show_signed_area: + fill_color = invert_color(color) + else: + fill_color = color + rect.set_fill(fill_color, opacity=fill_opacity) + rect.set_stroke(stroke_color, width=stroke_width) + rectangles.add(rect) + return rectangles + + def get_riemann_rectangles_list( + self, + graph, + n_iterations, + max_dx=0.5, + power_base=2, + stroke_width=1, + **kwargs + ): + return [ + self.get_riemann_rectangles( + graph=graph, + dx=float(max_dx) / (power_base**n), + stroke_width=float(stroke_width) / (power_base**n), + **kwargs + ) + for n in range(n_iterations) + ] + + def get_area(self, graph, t_min, t_max): + numerator = max(t_max - t_min, 0.0001) + dx = float(numerator) / self.num_rects + return self.get_riemann_rectangles( + graph, + x_min=t_min, + x_max=t_max, + dx=dx, + stroke_width=0, + ).set_fill(opacity=self.area_opacity) + + def transform_between_riemann_rects(self, curr_rects, new_rects, **kwargs): + transform_kwargs = { + "run_time": 2, + "lag_ratio": 0.5 + } + added_anims = kwargs.get("added_anims", []) + transform_kwargs.update(kwargs) + curr_rects.align_family(new_rects) + x_coords = set() # Keep track of new repetitions + for rect in curr_rects: + x = rect.get_center()[0] + if x in x_coords: + rect.set_fill(opacity=0) + else: + x_coords.add(x) + self.play( + Transform(curr_rects, new_rects, **transform_kwargs), + *added_anims + ) + + def get_vertical_line_to_graph( + self, + x, graph, + line_class=Line, + **line_kwargs + ): + if "color" not in line_kwargs: + line_kwargs["color"] = graph.get_color() + return line_class( + self.coords_to_point(x, 0), + self.input_to_graph_point(x, graph), + **line_kwargs + ) + + def get_vertical_lines_to_graph( + self, graph, + x_min=None, + x_max=None, + num_lines=20, + **kwargs + ): + x_min = x_min or self.x_min + x_max = x_max or self.x_max + return VGroup(*[ + self.get_vertical_line_to_graph(x, graph, **kwargs) + for x in np.linspace(x_min, x_max, num_lines) + ]) + + def get_secant_slope_group( + self, + x, graph, + dx=None, + dx_line_color=None, + df_line_color=None, + dx_label=None, + df_label=None, + include_secant_line=True, + secant_line_color=None, + secant_line_length=10, + ): + """ + Resulting group is of the form VGroup( + dx_line, + df_line, + dx_label, (if applicable) + df_label, (if applicable) + secant_line, (if applicable) + ) + with attributes of those names. + """ + kwargs = locals() + kwargs.pop("self") + group = VGroup() + group.kwargs = kwargs + + dx = dx or float(self.x_max - self.x_min) / 10 + dx_line_color = dx_line_color or self.default_input_color + df_line_color = df_line_color or graph.get_color() + + p1 = self.input_to_graph_point(x, graph) + p2 = self.input_to_graph_point(x + dx, graph) + interim_point = p2[0] * RIGHT + p1[1] * UP + + group.dx_line = Line( + p1, interim_point, + color=dx_line_color + ) + group.df_line = Line( + interim_point, p2, + color=df_line_color + ) + group.add(group.dx_line, group.df_line) + + labels = VGroup() + if dx_label is not None: + group.dx_label = Tex(dx_label) + labels.add(group.dx_label) + group.add(group.dx_label) + if df_label is not None: + group.df_label = Tex(df_label) + labels.add(group.df_label) + group.add(group.df_label) + + if len(labels) > 0: + max_width = 0.8 * group.dx_line.get_width() + max_height = 0.8 * group.df_line.get_height() + if labels.get_width() > max_width: + labels.set_width(max_width) + if labels.get_height() > max_height: + labels.set_height(max_height) + + if dx_label is not None: + group.dx_label.next_to( + group.dx_line, + np.sign(dx) * DOWN, + buff=group.dx_label.get_height() / 2 + ) + group.dx_label.set_color(group.dx_line.get_color()) + + if df_label is not None: + group.df_label.next_to( + group.df_line, + np.sign(dx) * RIGHT, + buff=group.df_label.get_height() / 2 + ) + group.df_label.set_color(group.df_line.get_color()) + + if include_secant_line: + secant_line_color = secant_line_color or self.default_derivative_color + group.secant_line = Line(p1, p2, color=secant_line_color) + group.secant_line.scale( + secant_line_length / group.secant_line.get_length() + ) + group.add(group.secant_line) + + return group + + def add_T_label(self, x_val, side=RIGHT, label=None, color=WHITE, animated=False, **kwargs): + triangle = RegularPolygon(n=3, start_angle=np.pi / 2) + triangle.set_height(MED_SMALL_BUFF) + triangle.move_to(self.coords_to_point(x_val, 0), UP) + triangle.set_fill(color, 1) + triangle.set_stroke(width=0) + if label is None: + T_label = Tex(self.variable_point_label, fill_color=color) + else: + T_label = Tex(label, fill_color=color) + + T_label.next_to(triangle, DOWN) + v_line = self.get_vertical_line_to_graph( + x_val, self.v_graph, + color=YELLOW + ) + + if animated: + self.play( + DrawBorderThenFill(triangle), + ShowCreation(v_line), + Write(T_label, run_time=1), + **kwargs + ) + + if np.all(side == LEFT): + self.left_T_label_group = VGroup(T_label, triangle) + self.left_v_line = v_line + self.add(self.left_T_label_group, self.left_v_line) + elif np.all(side == RIGHT): + self.right_T_label_group = VGroup(T_label, triangle) + self.right_v_line = v_line + self.add(self.right_T_label_group, self.right_v_line) + + def get_animation_integral_bounds_change( + self, + graph, + new_t_min, + new_t_max, + fade_close_to_origin=True, + run_time=1.0 + ): + curr_t_min = self.x_axis.point_to_number(self.area.get_left()) + curr_t_max = self.x_axis.point_to_number(self.area.get_right()) + if new_t_min is None: + new_t_min = curr_t_min + if new_t_max is None: + new_t_max = curr_t_max + + group = VGroup(self.area) + group.add(self.left_v_line) + group.add(self.left_T_label_group) + group.add(self.right_v_line) + group.add(self.right_T_label_group) + + def update_group(group, alpha): + area, left_v_line, left_T_label, right_v_line, right_T_label = group + t_min = interpolate(curr_t_min, new_t_min, alpha) + t_max = interpolate(curr_t_max, new_t_max, alpha) + new_area = self.get_area(graph, t_min, t_max) + + new_left_v_line = self.get_vertical_line_to_graph( + t_min, graph + ) + new_left_v_line.set_color(left_v_line.get_color()) + left_T_label.move_to(new_left_v_line.get_bottom(), UP) + + new_right_v_line = self.get_vertical_line_to_graph( + t_max, graph + ) + new_right_v_line.set_color(right_v_line.get_color()) + right_T_label.move_to(new_right_v_line.get_bottom(), UP) + + # Fade close to 0 + if fade_close_to_origin: + if len(left_T_label) > 0: + left_T_label[0].set_fill(opacity=min(1, np.abs(t_min))) + if len(right_T_label) > 0: + right_T_label[0].set_fill(opacity=min(1, np.abs(t_max))) + + Transform(area, new_area).update(1) + Transform(left_v_line, new_left_v_line).update(1) + Transform(right_v_line, new_right_v_line).update(1) + return group + + return UpdateFromAlphaFunc(group, update_group, run_time=run_time) + + def animate_secant_slope_group_change( + self, secant_slope_group, + target_dx=None, + target_x=None, + run_time=3, + added_anims=None, + **anim_kwargs + ): + if target_dx is None and target_x is None: + raise Exception( + "At least one of target_x and target_dx must not be None") + if added_anims is None: + added_anims = [] + + start_dx = secant_slope_group.kwargs["dx"] + start_x = secant_slope_group.kwargs["x"] + if target_dx is None: + target_dx = start_dx + if target_x is None: + target_x = start_x + + def update_func(group, alpha): + dx = interpolate(start_dx, target_dx, alpha) + x = interpolate(start_x, target_x, alpha) + kwargs = dict(secant_slope_group.kwargs) + kwargs["dx"] = dx + kwargs["x"] = x + new_group = self.get_secant_slope_group(**kwargs) + group.become(new_group) + return group + + self.play( + UpdateFromAlphaFunc( + secant_slope_group, update_func, + run_time=run_time, + **anim_kwargs + ), + *added_anims + ) + secant_slope_group.kwargs["x"] = target_x + secant_slope_group.kwargs["dx"] = target_dx diff --git a/once_useful_constructs/graph_theory.py b/once_useful_constructs/graph_theory.py new file mode 100644 index 0000000..9baee42 --- /dev/null +++ b/once_useful_constructs/graph_theory.py @@ -0,0 +1,413 @@ +from functools import reduce +import itertools as it +import operator as op + +import numpy as np + +from manimlib.constants import * +from manimlib.scene.scene import Scene +from manimlib.utils.rate_functions import there_and_back +from manimlib.utils.space_ops import center_of_mass + + +class Graph(): + def __init__(self): + # List of points in R^3 + # vertices = [] + # List of pairs of indices of vertices + # edges = [] + # List of tuples of indices of vertices. The last should + # be a cycle whose interior is the entire graph, and when + # regions are computed its complement will be taken. + # region_cycles = [] + + self.construct() + + def construct(self): + pass + + def __str__(self): + return self.__class__.__name__ + + +class CubeGraph(Graph): + """ + 5 7 + 12 + 03 + 4 6 + """ + + def construct(self): + self.vertices = [ + (x, y, 0) + for r in (1, 2) + for x, y in it.product([-r, r], [-r, r]) + ] + self.edges = [ + (0, 1), + (0, 2), + (3, 1), + (3, 2), + (4, 5), + (4, 6), + (7, 5), + (7, 6), + (0, 4), + (1, 5), + (2, 6), + (3, 7), + ] + self.region_cycles = [ + [0, 2, 3, 1], + [4, 0, 1, 5], + [4, 6, 2, 0], + [6, 7, 3, 2], + [7, 5, 1, 3], + [4, 6, 7, 5], # By convention, last region will be "outside" + ] + + +class SampleGraph(Graph): + """ + 4 2 3 8 + 0 1 + 7 + 5 6 + """ + + def construct(self): + self.vertices = [ + (0, 0, 0), + (2, 0, 0), + (1, 2, 0), + (3, 2, 0), + (-1, 2, 0), + (-2, -2, 0), + (2, -2, 0), + (4, -1, 0), + (6, 2, 0), + ] + self.edges = [ + (0, 1), + (1, 2), + (1, 3), + (3, 2), + (2, 4), + (4, 0), + (2, 0), + (4, 5), + (0, 5), + (1, 5), + (5, 6), + (6, 7), + (7, 1), + (7, 8), + (8, 3), + ] + self.region_cycles = [ + (0, 1, 2), + (1, 3, 2), + (2, 4, 0), + (4, 5, 0), + (0, 5, 1), + (1, 5, 6, 7), + (1, 7, 8, 3), + (4, 5, 6, 7, 8, 3, 2), + ] + + +class OctohedronGraph(Graph): + """ + 3 + + 1 0 + 2 + 4 5 + """ + + def construct(self): + self.vertices = [ + (r * np.cos(angle), r * np.sin(angle) - 1, 0) + for r, s in [(1, 0), (3, 3)] + for angle in (np.pi / 6) * np.array([s, 4 + s, 8 + s]) + ] + self.edges = [ + (0, 1), + (1, 2), + (2, 0), + (5, 0), + (0, 3), + (3, 5), + (3, 1), + (3, 4), + (1, 4), + (4, 2), + (4, 5), + (5, 2), + ] + self.region_cycles = [ + (0, 1, 2), + (0, 5, 3), + (3, 1, 0), + (3, 4, 1), + (1, 4, 2), + (2, 4, 5), + (5, 0, 2), + (3, 4, 5), + ] + + +class CompleteGraph(Graph): + def __init__(self, num_vertices, radius=3): + self.num_vertices = num_vertices + self.radius = radius + Graph.__init__(self) + + def construct(self): + self.vertices = [ + (self.radius * np.cos(theta), self.radius * np.sin(theta), 0) + for x in range(self.num_vertices) + for theta in [2 * np.pi * x / self.num_vertices] + ] + self.edges = it.combinations(list(range(self.num_vertices)), 2) + + def __str__(self): + return Graph.__str__(self) + str(self.num_vertices) + + +class DiscreteGraphScene(Scene): + args_list = [ + (CubeGraph(),), + (SampleGraph(),), + (OctohedronGraph(),), + ] + + @staticmethod + def args_to_string(*args): + return str(args[0]) + + def __init__(self, graph, *args, **kwargs): + # See CubeGraph() above for format of graph + self.graph = graph + Scene.__init__(self, *args, **kwargs) + + def construct(self): + self._points = list(map(np.array, self.graph.vertices)) + self.vertices = self.dots = [Dot(p) for p in self._points] + self.edges = self.lines = [ + Line(self._points[i], self._points[j]) + for i, j in self.graph.edges + ] + self.add(*self.dots + self.edges) + + def generate_regions(self): + regions = [ + self.region_from_cycle(cycle) + for cycle in self.graph.region_cycles + ] + regions[-1].complement() # Outer region painted outwardly... + self.regions = regions + + def region_from_cycle(self, cycle): + point_pairs = [ + [ + self._points[cycle[i]], + self._points[cycle[(i + 1) % len(cycle)]] + ] + for i in range(len(cycle)) + ] + return region_from_line_boundary( + *point_pairs, shape=self.shape + ) + + def draw_vertices(self, **kwargs): + self.clear() + self.play(ShowCreation(Mobject(*self.vertices), **kwargs)) + + def draw_edges(self): + self.play(*[ + ShowCreation(edge, run_time=1.0) + for edge in self.edges + ]) + + def accent_vertices(self, **kwargs): + self.remove(*self.vertices) + start = Mobject(*self.vertices) + end = Mobject(*[ + Dot(point, radius=3 * Dot.DEFAULT_RADIUS, color="lightgreen") + for point in self._points + ]) + self.play(Transform( + start, end, rate_func=there_and_back, + **kwargs + )) + self.remove(start) + self.add(*self.vertices) + + def replace_vertices_with(self, mobject): + mobject.center() + diameter = max(mobject.get_height(), mobject.get_width()) + self.play(*[ + CounterclockwiseTransform( + vertex, + mobject.copy().shift(vertex.get_center()) + ) + for vertex in self.vertices + ] + [ + ApplyMethod( + edge.scale, + (edge.get_length() - diameter) / edge.get_length() + ) + for edge in self.edges + ]) + + def annotate_edges(self, mobject, fade_in=True, **kwargs): + angles = list(map(np.arctan, list(map(Line.get_slope, self.edges)))) + self.edge_annotations = [ + mobject.copy().rotate(angle).move_to(edge.get_center()) + for angle, edge in zip(angles, self.edges) + ] + if fade_in: + self.play(*[ + FadeIn(ann, **kwargs) + for ann in self.edge_annotations + ]) + + def trace_cycle(self, cycle=None, color="yellow", run_time=2.0): + if cycle is None: + cycle = self.graph.region_cycles[0] + next_in_cycle = it.cycle(cycle) + next(next_in_cycle) # jump one ahead + self.traced_cycle = Mobject(*[ + Line(self._points[i], self._points[j]).set_color(color) + for i, j in zip(cycle, next_in_cycle) + ]) + self.play( + ShowCreation(self.traced_cycle), + run_time=run_time + ) + + def generate_spanning_tree(self, root=0, color="yellow"): + self.spanning_tree_root = 0 + pairs = deepcopy(self.graph.edges) + pairs += [tuple(reversed(pair)) for pair in pairs] + self.spanning_tree_index_pairs = [] + curr = root + spanned_vertices = set([curr]) + to_check = set([curr]) + while len(to_check) > 0: + curr = to_check.pop() + for pair in pairs: + if pair[0] == curr and pair[1] not in spanned_vertices: + self.spanning_tree_index_pairs.append(pair) + spanned_vertices.add(pair[1]) + to_check.add(pair[1]) + self.spanning_tree = Mobject(*[ + Line( + self._points[pair[0]], + self._points[pair[1]] + ).set_color(color) + for pair in self.spanning_tree_index_pairs + ]) + + def generate_treeified_spanning_tree(self): + bottom = -FRAME_Y_RADIUS + 1 + x_sep = 1 + y_sep = 2 + if not hasattr(self, "spanning_tree"): + self.generate_spanning_tree() + root = self.spanning_tree_root + color = self.spanning_tree.get_color() + indices = list(range(len(self._points))) + # Build dicts + parent_of = dict([ + tuple(reversed(pair)) + for pair in self.spanning_tree_index_pairs + ]) + children_of = dict([(index, []) for index in indices]) + for child in parent_of: + children_of[parent_of[child]].append(child) + + x_coord_of = {root: 0} + y_coord_of = {root: bottom} + # width to allocate to a given node, computed as + # the maximum number of decendents in a single generation, + # minus 1, multiplied by x_sep + width_of = {} + for index in indices: + next_generation = children_of[index] + curr_max = max(1, len(next_generation)) + while next_generation != []: + next_generation = reduce(op.add, [ + children_of[node] + for node in next_generation + ]) + curr_max = max(curr_max, len(next_generation)) + width_of[index] = x_sep * (curr_max - 1) + to_process = [root] + while to_process != []: + index = to_process.pop() + if index not in y_coord_of: + y_coord_of[index] = y_sep + y_coord_of[parent_of[index]] + children = children_of[index] + left_hand = x_coord_of[index] - width_of[index] / 2.0 + for child in children: + x_coord_of[child] = left_hand + width_of[child] / 2.0 + left_hand += width_of[child] + x_sep + to_process += children + + new_points = [ + np.array([ + x_coord_of[index], + y_coord_of[index], + 0 + ]) + for index in indices + ] + self.treeified_spanning_tree = Mobject(*[ + Line(new_points[i], new_points[j]).set_color(color) + for i, j in self.spanning_tree_index_pairs + ]) + + def generate_dual_graph(self): + point_at_infinity = np.array([np.inf] * 3) + cycles = self.graph.region_cycles + self.dual_points = [ + center_of_mass([ + self._points[index] + for index in cycle + ]) + for cycle in cycles + ] + self.dual_vertices = [ + Dot(point).set_color("green") + for point in self.dual_points + ] + self.dual_vertices[-1] = Circle().scale(FRAME_X_RADIUS + FRAME_Y_RADIUS) + self.dual_points[-1] = point_at_infinity + + self.dual_edges = [] + for pair in self.graph.edges: + dual_point_pair = [] + for cycle in cycles: + if not (pair[0] in cycle and pair[1] in cycle): + continue + index1, index2 = cycle.index(pair[0]), cycle.index(pair[1]) + if abs(index1 - index2) in [1, len(cycle) - 1]: + dual_point_pair.append( + self.dual_points[cycles.index(cycle)] + ) + assert(len(dual_point_pair) == 2) + for i in 0, 1: + if all(dual_point_pair[i] == point_at_infinity): + new_point = np.array(dual_point_pair[1 - i]) + vect = center_of_mass([ + self._points[pair[0]], + self._points[pair[1]] + ]) - new_point + new_point += FRAME_X_RADIUS * vect / get_norm(vect) + dual_point_pair[i] = new_point + self.dual_edges.append( + Line(*dual_point_pair).set_color() + ) diff --git a/so_old_as_to_ignore/homeless.py b/once_useful_constructs/homeless.py similarity index 100% rename from so_old_as_to_ignore/homeless.py rename to once_useful_constructs/homeless.py diff --git a/once_useful_constructs/light.py b/once_useful_constructs/light.py new file mode 100644 index 0000000..5ec132b --- /dev/null +++ b/once_useful_constructs/light.py @@ -0,0 +1,602 @@ +from traceback import * + +from scipy.spatial import ConvexHull + +from manimlib.animation.composition import LaggedStartMap +from manimlib.animation.fading import FadeIn +from manimlib.animation.fading import FadeOut +from manimlib.animation.transform import Transform +from manimlib.constants import * +from manimlib.mobject.geometry import AnnularSector +from manimlib.mobject.geometry import Annulus +from manimlib.mobject.svg.svg_mobject import SVGMobject +from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.mobject.types.vectorized_mobject import VectorizedPoint +from manimlib.utils.space_ops import angle_between_vectors +from manimlib.utils.space_ops import project_along_vector +from manimlib.utils.space_ops import rotate_vector +from manimlib.utils.space_ops import z_to_vector + +LIGHT_COLOR = YELLOW +SHADOW_COLOR = BLACK +SWITCH_ON_RUN_TIME = 1.5 +FAST_SWITCH_ON_RUN_TIME = 0.1 +NUM_LEVELS = 30 +NUM_CONES = 7 # in first lighthouse scene +NUM_VISIBLE_CONES = 5 # ibidem +ARC_TIP_LENGTH = 0.2 +AMBIENT_FULL = 0.8 +AMBIENT_DIMMED = 0.5 +SPOTLIGHT_FULL = 0.8 +SPOTLIGHT_DIMMED = 0.5 +LIGHTHOUSE_HEIGHT = 0.8 + +DEGREES = TAU / 360 + + +def inverse_power_law(maxint, scale, cutoff, exponent): + return (lambda r: maxint * (cutoff / (r / scale + cutoff))**exponent) + + +def inverse_quadratic(maxint, scale, cutoff): + return inverse_power_law(maxint, scale, cutoff, 2) + + +class SwitchOn(LaggedStartMap): + CONFIG = { + "lag_ratio": 0.2, + "run_time": SWITCH_ON_RUN_TIME + } + + def __init__(self, light, **kwargs): + if (not isinstance(light, AmbientLight) and not isinstance(light, Spotlight)): + raise Exception( + "Only AmbientLights and Spotlights can be switched on") + LaggedStartMap.__init__( + self, FadeIn, light, **kwargs + ) + + +class SwitchOff(LaggedStartMap): + CONFIG = { + "lag_ratio": 0.2, + "run_time": SWITCH_ON_RUN_TIME + } + + def __init__(self, light, **kwargs): + if (not isinstance(light, AmbientLight) and not isinstance(light, Spotlight)): + raise Exception( + "Only AmbientLights and Spotlights can be switched off") + light.set_submobjects(light.submobjects[::-1]) + LaggedStartMap.__init__(self, FadeOut, light, **kwargs) + light.set_submobjects(light.submobjects[::-1]) + + +class Lighthouse(SVGMobject): + CONFIG = { + "height": LIGHTHOUSE_HEIGHT, + "fill_color": WHITE, + "fill_opacity": 1.0, + } + + def __init__(self, **kwargs): + super().__init__("lighthouse", **kwargs) + + def move_to(self, point): + self.next_to(point, DOWN, buff=0) + + +class AmbientLight(VMobject): + + # Parameters are: + # * a source point + # * an opacity function + # * a light color + # * a max opacity + # * a radius (larger than the opacity's dropoff length) + # * the number of subdivisions (levels, annuli) + + CONFIG = { + "source_point": VectorizedPoint(location=ORIGIN, stroke_width=0, fill_opacity=0), + "opacity_function": lambda r: 1.0 / (r + 1.0)**2, + "color": LIGHT_COLOR, + "max_opacity": 1.0, + "num_levels": NUM_LEVELS, + "radius": 5.0 + } + + def init_points(self): + # in theory, this method is only called once, right? + # so removing submobs shd not be necessary + # + # Note: Usually, yes, it is only called within Mobject.__init__, + # but there is no strong guarantee of that, and you may want certain + # update functions to regenerate points here and there. + for submob in self.submobjects: + self.remove(submob) + + self.add(self.source_point) + + # create annuli + self.radius = float(self.radius) + dr = self.radius / self.num_levels + for r in np.arange(0, self.radius, dr): + alpha = self.max_opacity * self.opacity_function(r) + annulus = Annulus( + inner_radius=r, + outer_radius=r + dr, + color=self.color, + fill_opacity=alpha + ) + annulus.move_to(self.get_source_point()) + self.add(annulus) + + def move_source_to(self, point): + # old_source_point = self.get_source_point() + # self.shift(point - old_source_point) + self.move_to(point) + + return self + + def get_source_point(self): + return self.source_point.get_location() + + def dimming(self, new_alpha): + old_alpha = self.max_opacity + self.max_opacity = new_alpha + for submob in self.submobjects: + old_submob_alpha = submob.fill_opacity + new_submob_alpha = old_submob_alpha * new_alpha / old_alpha + submob.set_fill(opacity=new_submob_alpha) + + +class Spotlight(VMobject): + CONFIG = { + "source_point": VectorizedPoint(location=ORIGIN, stroke_width=0, fill_opacity=0), + "opacity_function": lambda r: 1.0 / (r / 2 + 1.0)**2, + "color": GREEN, # LIGHT_COLOR, + "max_opacity": 1.0, + "num_levels": 10, + "radius": 10.0, + "screen": None, + "camera_mob": None + } + + def projection_direction(self): + # Note: This seems reasonable, though for it to work you'd + # need to be sure that any 3d scene including a spotlight + # somewhere assigns that spotlights "camera" attribute + # to be the camera associated with that scene. + if self.camera_mob is None: + return OUT + else: + [phi, theta, r] = self.camera_mob.get_center() + v = np.array([np.sin(phi) * np.cos(theta), + np.sin(phi) * np.sin(theta), np.cos(phi)]) + return v # /get_norm(v) + + def project(self, point): + v = self.projection_direction() + w = project_along_vector(point, v) + return w + + def get_source_point(self): + return self.source_point.get_location() + + def init_points(self): + self.set_submobjects([]) + + self.add(self.source_point) + + if self.screen is not None: + # look for the screen and create annular sectors + lower_angle, upper_angle = self.viewing_angles(self.screen) + self.radius = float(self.radius) + dr = self.radius / self.num_levels + lower_ray, upper_ray = self.viewing_rays(self.screen) + + for r in np.arange(0, self.radius, dr): + new_sector = self.new_sector(r, dr, lower_angle, upper_angle) + self.add(new_sector) + + def new_sector(self, r, dr, lower_angle, upper_angle): + alpha = self.max_opacity * self.opacity_function(r) + annular_sector = AnnularSector( + inner_radius=r, + outer_radius=r + dr, + color=self.color, + fill_opacity=alpha, + start_angle=lower_angle, + angle=upper_angle - lower_angle + ) + # rotate (not project) it into the viewing plane + rotation_matrix = z_to_vector(self.projection_direction()) + annular_sector.apply_matrix(rotation_matrix) + # now rotate it inside that plane + rotated_RIGHT = np.dot(RIGHT, rotation_matrix.T) + projected_RIGHT = self.project(RIGHT) + omega = angle_between_vectors(rotated_RIGHT, projected_RIGHT) + annular_sector.rotate(omega, axis=self.projection_direction()) + annular_sector.move_arc_center_to(self.get_source_point()) + + return annular_sector + + def viewing_angle_of_point(self, point): + # as measured from the positive x-axis + v1 = self.project(RIGHT) + v2 = self.project(np.array(point) - self.get_source_point()) + absolute_angle = angle_between_vectors(v1, v2) + # determine the angle's sign depending on their plane's + # choice of orientation. That choice is set by the camera + # position, i. e. projection direction + + if np.dot(self.projection_direction(), np.cross(v1, v2)) > 0: + return absolute_angle + else: + return -absolute_angle + + def viewing_angles(self, screen): + + screen_points = screen.get_anchors() + projected_screen_points = list(map(self.project, screen_points)) + + viewing_angles = np.array(list(map(self.viewing_angle_of_point, + projected_screen_points))) + + lower_angle = upper_angle = 0 + if len(viewing_angles) != 0: + lower_angle = np.min(viewing_angles) + upper_angle = np.max(viewing_angles) + + if upper_angle - lower_angle > TAU / 2: + lower_angle, upper_angle = upper_angle, lower_angle + TAU + return lower_angle, upper_angle + + def viewing_rays(self, screen): + + lower_angle, upper_angle = self.viewing_angles(screen) + projected_RIGHT = self.project( + RIGHT) / get_norm(self.project(RIGHT)) + lower_ray = rotate_vector( + projected_RIGHT, lower_angle, axis=self.projection_direction()) + upper_ray = rotate_vector( + projected_RIGHT, upper_angle, axis=self.projection_direction()) + + return lower_ray, upper_ray + + def opening_angle(self): + l, u = self.viewing_angles(self.screen) + return u - l + + def start_angle(self): + l, u = self.viewing_angles(self.screen) + return l + + def stop_angle(self): + l, u = self.viewing_angles(self.screen) + return u + + def move_source_to(self, point): + self.source_point.set_location(np.array(point)) + # self.source_point.move_to(np.array(point)) + # self.move_to(point) + self.update_sectors() + return self + + def update_sectors(self): + if self.screen is None: + return + for submob in self.submobjects: + if type(submob) == AnnularSector: + lower_angle, upper_angle = self.viewing_angles(self.screen) + # dr = submob.outer_radius - submob.inner_radius + dr = self.radius / self.num_levels + new_submob = self.new_sector( + submob.inner_radius, dr, lower_angle, upper_angle + ) + # submob.points = new_submob.points + # submob.set_fill(opacity = 10 * self.opacity_function(submob.outer_radius)) + Transform(submob, new_submob).update(1) + + def dimming(self, new_alpha): + old_alpha = self.max_opacity + self.max_opacity = new_alpha + for submob in self.submobjects: + # Note: Maybe it'd be best to have a Shadow class so that the + # type can be checked directly? + if type(submob) != AnnularSector: + # it's the shadow, don't dim it + continue + old_submob_alpha = submob.fill_opacity + new_submob_alpha = old_submob_alpha * new_alpha / old_alpha + submob.set_fill(opacity=new_submob_alpha) + + def change_opacity_function(self, new_f): + self.opacity_function = new_f + dr = self.radius / self.num_levels + + sectors = [] + for submob in self.submobjects: + if type(submob) == AnnularSector: + sectors.append(submob) + + for (r, submob) in zip(np.arange(0, self.radius, dr), sectors): + if type(submob) != AnnularSector: + # it's the shadow, don't dim it + continue + alpha = self.opacity_function(r) + submob.set_fill(opacity=alpha) + +# Warning: This class is likely quite buggy. + + +class LightSource(VMobject): + # combines: + # a lighthouse + # an ambient light + # a spotlight + # and a shadow + CONFIG = { + "source_point": VectorizedPoint(location=ORIGIN, stroke_width=0, fill_opacity=0), + "color": LIGHT_COLOR, + "num_levels": 10, + "radius": 10.0, + "screen": None, + "opacity_function": inverse_quadratic(1, 2, 1), + "max_opacity_ambient": AMBIENT_FULL, + "max_opacity_spotlight": SPOTLIGHT_FULL, + "camera_mob": None + } + + def init_points(self): + + self.add(self.source_point) + + self.lighthouse = Lighthouse() + self.ambient_light = AmbientLight( + source_point=VectorizedPoint(location=self.get_source_point()), + color=self.color, + num_levels=self.num_levels, + radius=self.radius, + opacity_function=self.opacity_function, + max_opacity=self.max_opacity_ambient + ) + if self.has_screen(): + self.spotlight = Spotlight( + source_point=VectorizedPoint(location=self.get_source_point()), + color=self.color, + num_levels=self.num_levels, + radius=self.radius, + screen=self.screen, + opacity_function=self.opacity_function, + max_opacity=self.max_opacity_spotlight, + camera_mob=self.camera_mob + ) + else: + self.spotlight = Spotlight() + + self.shadow = VMobject(fill_color=SHADOW_COLOR, + fill_opacity=1.0, stroke_color=BLACK) + self.lighthouse.next_to(self.get_source_point(), DOWN, buff=0) + self.ambient_light.move_source_to(self.get_source_point()) + + if self.has_screen(): + self.spotlight.move_source_to(self.get_source_point()) + self.update_shadow() + + self.add(self.ambient_light, self.spotlight, + self.lighthouse, self.shadow) + + def has_screen(self): + if self.screen is None: + return False + elif self.screen.get_num_points() == 0: + return False + else: + return True + + def dim_ambient(self): + self.set_max_opacity_ambient(AMBIENT_DIMMED) + + def set_max_opacity_ambient(self, new_opacity): + self.max_opacity_ambient = new_opacity + self.ambient_light.dimming(new_opacity) + + def dim_spotlight(self): + self.set_max_opacity_spotlight(SPOTLIGHT_DIMMED) + + def set_max_opacity_spotlight(self, new_opacity): + self.max_opacity_spotlight = new_opacity + self.spotlight.dimming(new_opacity) + + def set_camera_mob(self, new_cam_mob): + self.camera_mob = new_cam_mob + self.spotlight.camera_mob = new_cam_mob + + def set_screen(self, new_screen): + if self.has_screen(): + self.spotlight.screen = new_screen + else: + # Note: See below + index = self.submobjects.index(self.spotlight) + # camera_mob = self.spotlight.camera_mob + self.remove(self.spotlight) + self.spotlight = Spotlight( + source_point=VectorizedPoint(location=self.get_source_point()), + color=self.color, + num_levels=self.num_levels, + radius=self.radius, + screen=new_screen, + camera_mob=self.camera_mob, + opacity_function=self.opacity_function, + max_opacity=self.max_opacity_spotlight, + ) + self.spotlight.move_source_to(self.get_source_point()) + + # Note: This line will make spotlight show up at the end + # of the submojects list, which can make it show up on + # top of the shadow. To make it show up in the + # same spot, you could try the following line, + # where "index" is what I defined above: + self.submobjects.insert(index, self.spotlight) + # self.add(self.spotlight) + + # in any case + self.screen = new_screen + + def move_source_to(self, point): + apoint = np.array(point) + v = apoint - self.get_source_point() + # Note: As discussed, things stand to behave better if source + # point is a submobject, so that it automatically interpolates + # during an animation, and other updates can be defined wrt + # that source point's location + self.source_point.set_location(apoint) + # self.lighthouse.next_to(apoint,DOWN,buff = 0) + # self.ambient_light.move_source_to(apoint) + self.lighthouse.shift(v) + # self.ambient_light.shift(v) + self.ambient_light.move_source_to(apoint) + if self.has_screen(): + self.spotlight.move_source_to(apoint) + self.update() + return self + + def change_spotlight_opacity_function(self, new_of): + self.spotlight.change_opacity_function(new_of) + + def set_radius(self, new_radius): + self.radius = new_radius + self.ambient_light.radius = new_radius + self.spotlight.radius = new_radius + + def update(self): + self.update_lighthouse() + self.update_ambient() + self.spotlight.update_sectors() + self.update_shadow() + + def update_lighthouse(self): + self.lighthouse.move_to(self.get_source_point()) + # new_lh = Lighthouse() + # new_lh.move_to(ORIGIN) + # new_lh.apply_matrix(self.rotation_matrix()) + # new_lh.shift(self.get_source_point()) + # self.lighthouse.submobjects = new_lh.submobjects + + def update_ambient(self): + new_ambient_light = AmbientLight( + source_point=VectorizedPoint(location=ORIGIN), + color=self.color, + num_levels=self.num_levels, + radius=self.radius, + opacity_function=self.opacity_function, + max_opacity=self.max_opacity_ambient + ) + new_ambient_light.apply_matrix(self.rotation_matrix()) + new_ambient_light.move_source_to(self.get_source_point()) + self.ambient_light.set_submobjects(new_ambient_light.submobjects) + + def get_source_point(self): + return self.source_point.get_location() + + def rotation_matrix(self): + + if self.camera_mob is None: + return np.eye(3) + + phi = self.camera_mob.get_center()[0] + theta = self.camera_mob.get_center()[1] + + R1 = np.array([ + [1, 0, 0], + [0, np.cos(phi), -np.sin(phi)], + [0, np.sin(phi), np.cos(phi)] + ]) + + R2 = np.array([ + [np.cos(theta + TAU / 4), -np.sin(theta + TAU / 4), 0], + [np.sin(theta + TAU / 4), np.cos(theta + TAU / 4), 0], + [0, 0, 1] + ]) + + R = np.dot(R2, R1) + return R + + def update_shadow(self): + point = self.get_source_point() + projected_screen_points = [] + if not self.has_screen(): + return + for point in self.screen.get_anchors(): + projected_screen_points.append(self.spotlight.project(point)) + + projected_source = project_along_vector( + self.get_source_point(), self.spotlight.projection_direction()) + + projected_point_cloud_3d = np.append( + projected_screen_points, + np.reshape(projected_source, (1, 3)), + axis=0 + ) + # z_to_vector(self.spotlight.projection_direction()) + rotation_matrix = self.rotation_matrix() + back_rotation_matrix = rotation_matrix.T # i. e. its inverse + + rotated_point_cloud_3d = np.dot( + projected_point_cloud_3d, back_rotation_matrix.T) + # these points now should all have z = 0 + + point_cloud_2d = rotated_point_cloud_3d[:, :2] + # now we can compute the convex hull + hull_2d = ConvexHull(point_cloud_2d) # guaranteed to run ccw + hull = [] + + # we also need the projected source point + source_point_2d = np.dot(self.spotlight.project( + self.get_source_point()), back_rotation_matrix.T)[:2] + + index = 0 + for point in point_cloud_2d[hull_2d.vertices]: + if np.all(np.abs(point - source_point_2d) < 1.0e-6): + source_index = index + index += 1 + continue + point_3d = np.array([point[0], point[1], 0]) + hull.append(point_3d) + index += 1 + + hull_mobject = VMobject() + hull_mobject.set_points_as_corners(hull) + hull_mobject.apply_matrix(rotation_matrix) + + anchors = hull_mobject.get_anchors() + + # add two control points for the outer cone + if np.size(anchors) == 0: + self.shadow.resize_points(0) + return + + ray1 = anchors[source_index - 1] - projected_source + ray1 = ray1 / get_norm(ray1) * 100 + + ray2 = anchors[source_index] - projected_source + ray2 = ray2 / get_norm(ray2) * 100 + outpoint1 = anchors[source_index - 1] + ray1 + outpoint2 = anchors[source_index] + ray2 + + new_anchors = anchors[:source_index] + new_anchors = np.append(new_anchors, np.array( + [outpoint1, outpoint2]), axis=0) + new_anchors = np.append(new_anchors, anchors[source_index:], axis=0) + self.shadow.set_points_as_corners(new_anchors) + + # shift it closer to the camera so it is in front of the spotlight + self.shadow.mark_paths_closed = True + + +# Redefining what was once a ContinualAnimation class +# as a function +def ScreenTracker(light_source): + light_source.add_updater(lambda m: m.update()) + return light_source diff --git a/once_useful_constructs/matrix_multiplication.py b/once_useful_constructs/matrix_multiplication.py new file mode 100644 index 0000000..1968595 --- /dev/null +++ b/once_useful_constructs/matrix_multiplication.py @@ -0,0 +1,139 @@ +import numpy as np + +from manimlib.animation.creation import ShowCreation +from manimlib.animation.fading import FadeOut +from manimlib.animation.transform import ApplyMethod +from manimlib.animation.transform import Transform +from manimlib.constants import * +from manimlib.mobject.geometry import Circle +from manimlib.mobject.geometry import Line +from manimlib.mobject.matrix import Matrix +from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.scene.scene import Scene + + +class NumericalMatrixMultiplication(Scene): + left_matrix = [[1, 2], [3, 4]] + right_matrix = [[5, 6], [7, 8]] + use_parens = True + + def construct(self): + left_string_matrix, right_string_matrix = [ + np.array(matrix).astype("string") + for matrix in (self.left_matrix, self.right_matrix) + ] + if right_string_matrix.shape[0] != left_string_matrix.shape[1]: + raise Exception("Incompatible shapes for matrix multiplication") + + left = Matrix(left_string_matrix) + right = Matrix(right_string_matrix) + result = self.get_result_matrix( + left_string_matrix, right_string_matrix + ) + + self.organize_matrices(left, right, result) + self.animate_product(left, right, result) + + def get_result_matrix(self, left, right): + (m, k), n = left.shape, right.shape[1] + mob_matrix = np.array([VGroup()]).repeat(m * n).reshape((m, n)) + for a in range(m): + for b in range(n): + template = "(%s)(%s)" if self.use_parens else "%s%s" + parts = [ + prefix + template % (left[a][c], right[c][b]) + for c in range(k) + for prefix in ["" if c == 0 else "+"] + ] + mob_matrix[a][b] = Tex(parts, next_to_buff=0.1) + return Matrix(mob_matrix) + + def add_lines(self, left, right): + line_kwargs = { + "color": BLUE, + "stroke_width": 2, + } + left_rows = [ + VGroup(*row) for row in left.get_mob_matrix() + ] + h_lines = VGroup() + for row in left_rows[:-1]: + h_line = Line(row.get_left(), row.get_right(), **line_kwargs) + h_line.next_to(row, DOWN, buff=left.v_buff / 2.) + h_lines.add(h_line) + + right_cols = [ + VGroup(*col) for col in np.transpose(right.get_mob_matrix()) + ] + v_lines = VGroup() + for col in right_cols[:-1]: + v_line = Line(col.get_top(), col.get_bottom(), **line_kwargs) + v_line.next_to(col, RIGHT, buff=right.h_buff / 2.) + v_lines.add(v_line) + + self.play(ShowCreation(h_lines)) + self.play(ShowCreation(v_lines)) + self.wait() + self.show_frame() + + def organize_matrices(self, left, right, result): + equals = Tex("=") + everything = VGroup(left, right, equals, result) + everything.arrange() + everything.set_width(FRAME_WIDTH - 1) + self.add(everything) + + def animate_product(self, left, right, result): + l_matrix = left.get_mob_matrix() + r_matrix = right.get_mob_matrix() + result_matrix = result.get_mob_matrix() + circle = Circle( + radius=l_matrix[0][0].get_height(), + color=GREEN + ) + circles = VGroup(*[ + entry.get_point_mobject() + for entry in (l_matrix[0][0], r_matrix[0][0]) + ]) + (m, k), n = l_matrix.shape, r_matrix.shape[1] + for mob in result_matrix.flatten(): + mob.set_color(BLACK) + lagging_anims = [] + for a in range(m): + for b in range(n): + for c in range(k): + l_matrix[a][c].set_color(YELLOW) + r_matrix[c][b].set_color(YELLOW) + for c in range(k): + start_parts = VGroup( + l_matrix[a][c].copy(), + r_matrix[c][b].copy() + ) + result_entry = result_matrix[a][b].split()[c] + + new_circles = VGroup(*[ + circle.copy().shift(part.get_center()) + for part in start_parts.split() + ]) + self.play(Transform(circles, new_circles)) + self.play( + Transform( + start_parts, + result_entry.copy().set_color(YELLOW), + path_arc=-np.pi / 2, + lag_ratio=0, + ), + *lagging_anims + ) + result_entry.set_color(YELLOW) + self.remove(start_parts) + lagging_anims = [ + ApplyMethod(result_entry.set_color, WHITE) + ] + + for c in range(k): + l_matrix[a][c].set_color(WHITE) + r_matrix[c][b].set_color(WHITE) + self.play(FadeOut(circles), *lagging_anims) + self.wait() diff --git a/once_useful_constructs/reconfigurable_scene.py b/once_useful_constructs/reconfigurable_scene.py new file mode 100644 index 0000000..96e68d8 --- /dev/null +++ b/once_useful_constructs/reconfigurable_scene.py @@ -0,0 +1,66 @@ +from manimlib.animation.transform import Transform +from manimlib.constants import * +from manimlib.mobject.mobject import Mobject +from manimlib.scene.scene import Scene + + +class ReconfigurableScene(Scene): + """ + Note, this seems to no longer work as intented. + """ + CONFIG = { + "allow_recursion": True, + } + + def setup(self): + self.states = [] + self.num_recursions = 0 + + def transition_to_alt_config( + self, + return_to_original_configuration=True, + transformation_kwargs=None, + **new_config + ): + if transformation_kwargs is None: + transformation_kwargs = {} + original_state = self.get_state() + state_copy = original_state.copy() + self.states.append(state_copy) + if not self.allow_recursion: + return + alt_scene = self.__class__( + skip_animations=True, + allow_recursion=False, + **new_config + ) + alt_state = alt_scene.states[len(self.states) - 1] + + if return_to_original_configuration: + self.clear() + self.transition_between_states( + state_copy, alt_state, + **transformation_kwargs + ) + self.transition_between_states( + state_copy, original_state, + **transformation_kwargs + ) + self.clear() + self.add(*original_state) + else: + self.transition_between_states( + original_state, alt_state, + **transformation_kwargs + ) + self.__dict__.update(new_config) + + def get_state(self): + # Want to return a mobject that maintains the most + # structure. The way to do that is to extract only + # those that aren't inside another. + return Mobject(*self.get_top_level_mobjects()) + + def transition_between_states(self, start_state, target_state, **kwargs): + self.play(Transform(start_state, target_state, **kwargs)) + self.wait() diff --git a/once_useful_constructs/region.py b/once_useful_constructs/region.py new file mode 100644 index 0000000..1bc95a0 --- /dev/null +++ b/once_useful_constructs/region.py @@ -0,0 +1,107 @@ +from copy import deepcopy +import itertools as it + +from manimlib.constants import * +from manimlib.mobject.mobject import Mobject +from manimlib.utils.iterables import adjacent_pairs + +# Warning: This is all now pretty deprecated, and should not be expected to work + + +class Region(Mobject): + CONFIG = { + "display_mode": "region" + } + + def __init__(self, condition=(lambda x, y: True), **kwargs): + """ + Condition must be a function which takes in two real + arrays (representing x and y values of space respectively) + and return a boolean array. This can essentially look like + a function from R^2 to {True, False}, but & and | must be + used in place of "and" and "or" + """ + Mobject.__init__(self, **kwargs) + self.condition = condition + + def _combine(self, region, op): + self.condition = lambda x, y: op( + self.condition(x, y), + region.condition(x, y) + ) + + def union(self, region): + self._combine(region, lambda bg1, bg2: bg1 | bg2) + return self + + def intersect(self, region): + self._combine(region, lambda bg1, bg2: bg1 & bg2) + return self + + def complement(self): + self.bool_grid = ~self.bool_grid + return self + + +class HalfPlane(Region): + def __init__(self, point_pair, upper_left=True, *args, **kwargs): + """ + point_pair of the form [(x_0, y_0,...), (x_1, y_1,...)] + + Pf upper_left is True, the side of the region will be + everything on the upper left side of the line through + the point pair + """ + if not upper_left: + point_pair = list(point_pair) + point_pair.reverse() + (x0, y0), (x1, y1) = point_pair[0][:2], point_pair[1][:2] + + def condition(x, y): + return (x1 - x0) * (y - y0) > (y1 - y0) * (x - x0) + Region.__init__(self, condition, *args, **kwargs) + + +def region_from_line_boundary(*lines, **kwargs): + reg = Region(**kwargs) + for line in lines: + reg.intersect(HalfPlane(line, **kwargs)) + return reg + + +def region_from_polygon_vertices(*vertices, **kwargs): + return region_from_line_boundary(*adjacent_pairs(vertices), **kwargs) + + +def plane_partition(*lines, **kwargs): + """ + A 'line' is a pair of points [(x0, y0,...), (x1, y1,...)] + + Returns the list of regions of the plane cut out by + these lines + """ + result = [] + half_planes = [HalfPlane(line, **kwargs) for line in lines] + complements = [deepcopy(hp).complement() for hp in half_planes] + num_lines = len(lines) + for bool_list in it.product(*[[True, False]] * num_lines): + reg = Region(**kwargs) + for i in range(num_lines): + if bool_list[i]: + reg.intersect(half_planes[i]) + else: + reg.intersect(complements[i]) + if reg.bool_grid.any(): + result.append(reg) + return result + + +def plane_partition_from_points(*points, **kwargs): + """ + Returns list of regions cut out by the complete graph + with points from the argument as vertices. + + Each point comes in the form (x, y) + """ + lines = [[p1, p2] for (p1, p2) in it.combinations(points, 2)] + return plane_partition(*lines, **kwargs) diff --git a/so_old_as_to_ignore/sed.py b/once_useful_constructs/sed.py similarity index 100% rename from so_old_as_to_ignore/sed.py rename to once_useful_constructs/sed.py diff --git a/once_useful_constructs/vector_space_scene.py b/once_useful_constructs/vector_space_scene.py new file mode 100644 index 0000000..b96eed4 --- /dev/null +++ b/once_useful_constructs/vector_space_scene.py @@ -0,0 +1,511 @@ +import numpy as np + +from manimlib.animation.animation import Animation +from manimlib.animation.creation import ShowCreation +from manimlib.animation.creation import Write +from manimlib.animation.fading import FadeOut +from manimlib.animation.growing import GrowArrow +from manimlib.animation.transform import ApplyFunction +from manimlib.animation.transform import ApplyPointwiseFunction +from manimlib.animation.transform import Transform +from manimlib.constants import BLACK, BLUE_D, GREEN_C, RED_C, GREY, WHITE, YELLOW +from manimlib.constants import DL, DOWN, ORIGIN, RIGHT, UP +from manimlib.constants import FRAME_WIDTH, FRAME_X_RADIUS, FRAME_Y_RADIUS +from manimlib.constants import SMALL_BUFF +from manimlib.mobject.coordinate_systems import Axes +from manimlib.mobject.coordinate_systems import NumberPlane +from manimlib.mobject.geometry import Arrow +from manimlib.mobject.geometry import Dot +from manimlib.mobject.geometry import Line +from manimlib.mobject.geometry import Rectangle +from manimlib.mobject.geometry import Vector +from manimlib.mobject.matrix import Matrix +from manimlib.mobject.matrix import VECTOR_LABEL_SCALE_FACTOR +from manimlib.mobject.matrix import vector_coordinate_label +from manimlib.mobject.mobject import Mobject +from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.mobject.svg.tex_mobject import TexText +from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.scene.scene import Scene +from manimlib.utils.rate_functions import rush_from +from manimlib.utils.rate_functions import rush_into +from manimlib.utils.space_ops import angle_of_vector +from manimlib.utils.space_ops import get_norm + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from manimlib.typing import ManimColor + from typing import List + +X_COLOR = GREEN_C +Y_COLOR = RED_C +Z_COLOR = BLUE_D + +# TODO: Much of this scene type seems dependent on the coordinate system chosen. +# That is, being centered at the origin with grid units corresponding to the +# arbitrary space units. Change it! +# +# Also, methods I would have thought of as getters, like coords_to_vector, are +# actually doing a lot of animating. +class VectorScene(Scene): + basis_vector_stroke_width: int = 6 + + def add_plane(self, animate=False, **kwargs): + plane = NumberPlane(**kwargs) + if animate: + self.play(ShowCreation(plane, lag_ratio=0.5)) + self.add(plane) + return plane + + def add_axes(self, animate=False, color=WHITE, **kwargs): + axes = Axes(color=color, tick_frequency=1) + if animate: + self.play(ShowCreation(axes)) + self.add(axes) + return axes + + def lock_in_faded_grid(self, dimness=0.7, axes_dimness=0.5): + plane = self.add_plane() + axes = plane.get_axes() + plane.fade(dimness) + axes.set_color(WHITE) + axes.fade(axes_dimness) + self.add(axes) + self.freeze_background() + + def get_vector(self, numerical_vector, **kwargs): + return Arrow( + self.plane.coords_to_point(0, 0), + self.plane.coords_to_point(*numerical_vector[:2]), + buff=0, + **kwargs + ) + + def add_vector(self, vector, color=YELLOW, animate=True, **kwargs): + if not isinstance(vector, Arrow): + vector = Vector(vector, color=color, **kwargs) + if animate: + self.play(GrowArrow(vector)) + self.add(vector) + return vector + + def write_vector_coordinates(self, vector, **kwargs): + coords = vector_coordinate_label(vector, **kwargs) + self.play(Write(coords)) + return coords + + def get_basis_vectors(self, i_hat_color=X_COLOR, j_hat_color=Y_COLOR): + return VGroup(*[ + Vector( + vect, + color=color, + stroke_width=self.basis_vector_stroke_width + ) + for vect, color in [ + ([1, 0], i_hat_color), + ([0, 1], j_hat_color) + ] + ]) + + def get_basis_vector_labels(self, **kwargs): + i_hat, j_hat = self.get_basis_vectors() + return VGroup(*[ + self.get_vector_label( + vect, label, color=color, + label_scale_factor=1, + **kwargs + ) + for vect, label, color in [ + (i_hat, "\\hat{\\imath}", X_COLOR), + (j_hat, "\\hat{\\jmath}", Y_COLOR), + ] + ]) + + def get_vector_label(self, vector, label, + at_tip=False, + direction="left", + rotate=False, + color=None, + label_scale_factor=VECTOR_LABEL_SCALE_FACTOR): + if not isinstance(label, Tex): + if len(label) == 1: + label = "\\vec{\\textbf{%s}}" % label + label = Tex(label) + if color is None: + color = vector.get_color() + label.set_color(color) + label.scale(label_scale_factor) + label.add_background_rectangle() + + if at_tip: + vect = vector.get_vector() + vect /= get_norm(vect) + label.next_to(vector.get_end(), vect, buff=SMALL_BUFF) + else: + angle = vector.get_angle() + if not rotate: + label.rotate(-angle, about_point=ORIGIN) + if direction == "left": + label.shift(-label.get_bottom() + 0.1 * UP) + else: + label.shift(-label.get_top() + 0.1 * DOWN) + label.rotate(angle, about_point=ORIGIN) + label.shift((vector.get_end() - vector.get_start()) / 2) + return label + + def label_vector(self, vector, label, animate=True, **kwargs): + label = self.get_vector_label(vector, label, **kwargs) + if animate: + self.play(Write(label, run_time=1)) + self.add(label) + return label + + def position_x_coordinate(self, x_coord, x_line, vector): + x_coord.next_to(x_line, -np.sign(vector[1]) * UP) + x_coord.set_color(X_COLOR) + return x_coord + + def position_y_coordinate(self, y_coord, y_line, vector): + y_coord.next_to(y_line, np.sign(vector[0]) * RIGHT) + y_coord.set_color(Y_COLOR) + return y_coord + + def coords_to_vector(self, vector, coords_start=2 * RIGHT + 2 * UP, clean_up=True): + starting_mobjects = list(self.mobjects) + array = Matrix(vector) + array.shift(coords_start) + arrow = Vector(vector) + x_line = Line(ORIGIN, vector[0] * RIGHT) + y_line = Line(x_line.get_end(), arrow.get_end()) + x_line.set_color(X_COLOR) + y_line.set_color(Y_COLOR) + x_coord, y_coord = array.get_mob_matrix().flatten() + + self.play(Write(array, run_time=1)) + self.wait() + self.play(ApplyFunction( + lambda x: self.position_x_coordinate(x, x_line, vector), + x_coord + )) + self.play(ShowCreation(x_line)) + self.play( + ApplyFunction( + lambda y: self.position_y_coordinate(y, y_line, vector), + y_coord + ), + FadeOut(array.get_brackets()) + ) + y_coord, brackets = self.get_mobjects_from_last_animation() + self.play(ShowCreation(y_line)) + self.play(ShowCreation(arrow)) + self.wait() + if clean_up: + self.clear() + self.add(*starting_mobjects) + + def vector_to_coords(self, vector, integer_labels=True, clean_up=True): + starting_mobjects = list(self.mobjects) + show_creation = False + if isinstance(vector, Arrow): + arrow = vector + vector = arrow.get_end()[:2] + else: + arrow = Vector(vector) + show_creation = True + array = vector_coordinate_label(arrow, integer_labels=integer_labels) + x_line = Line(ORIGIN, vector[0] * RIGHT) + y_line = Line(x_line.get_end(), arrow.get_end()) + x_line.set_color(X_COLOR) + y_line.set_color(Y_COLOR) + x_coord, y_coord = array.get_mob_matrix().flatten() + x_coord_start = self.position_x_coordinate( + x_coord.copy(), x_line, vector + ) + y_coord_start = self.position_y_coordinate( + y_coord.copy(), y_line, vector + ) + brackets = array.get_brackets() + + if show_creation: + self.play(ShowCreation(arrow)) + self.play( + ShowCreation(x_line), + Write(x_coord_start), + run_time=1 + ) + self.play( + ShowCreation(y_line), + Write(y_coord_start), + run_time=1 + ) + self.wait() + self.play( + Transform(x_coord_start, x_coord, lag_ratio=0), + Transform(y_coord_start, y_coord, lag_ratio=0), + Write(brackets, run_time=1), + ) + self.wait() + + self.remove(x_coord_start, y_coord_start, brackets) + self.add(array) + if clean_up: + self.clear() + self.add(*starting_mobjects) + return array, x_line, y_line + + def show_ghost_movement(self, vector): + if isinstance(vector, Arrow): + vector = vector.get_end() - vector.get_start() + elif len(vector) == 2: + vector = np.append(np.array(vector), 0.0) + x_max = int(FRAME_X_RADIUS + abs(vector[0])) + y_max = int(FRAME_Y_RADIUS + abs(vector[1])) + dots = VMobject(*[ + Dot(x * RIGHT + y * UP) + for x in range(-x_max, x_max) + for y in range(-y_max, y_max) + ]) + dots.set_fill(BLACK, opacity=0) + dots_halfway = dots.copy().shift(vector / 2).set_fill(WHITE, 1) + dots_end = dots.copy().shift(vector) + + self.play(Transform( + dots, dots_halfway, rate_func=rush_into + )) + self.play(Transform( + dots, dots_end, rate_func=rush_from + )) + self.remove(dots) + + +class LinearTransformationScene(VectorScene): + include_background_plane: bool = True + include_foreground_plane: bool = True + foreground_plane_kwargs: dict = dict( + x_max=FRAME_WIDTH / 2, + x_min=-FRAME_WIDTH / 2, + y_max=FRAME_WIDTH / 2, + y_min=-FRAME_WIDTH / 2, + faded_line_ratio=0 + ) + background_plane_kwargs: dict = dict( + color=GREY, + axis_config=dict(color=GREY), + background_line_style=dict( + stroke_color=GREY, + stroke_width=1, + ), + ) + show_coordinates: bool = True + show_basis_vectors: bool = True + basis_vector_stroke_width: float = 6.0 + i_hat_color: ManimColor = X_COLOR + j_hat_color: ManimColor = Y_COLOR + leave_ghost_vectors: bool = False + t_matrix: List[List[float]] = [[3, 0], [1, 2]] + + def setup(self): + # The has_already_setup attr is to not break all the old Scenes + if hasattr(self, "has_already_setup"): + return + self.has_already_setup = True + self.background_mobjects = [] + self.foreground_mobjects = [] + self.transformable_mobjects = [] + self.moving_vectors = [] + self.transformable_labels = [] + self.moving_mobjects = [] + + self.t_matrix = np.array(self.t_matrix) + self.background_plane = NumberPlane( + **self.background_plane_kwargs + ) + + if self.show_coordinates: + self.background_plane.add_coordinates() + if self.include_background_plane: + self.add_background_mobject(self.background_plane) + if self.include_foreground_plane: + self.plane = NumberPlane(**self.foreground_plane_kwargs) + self.add_transformable_mobject(self.plane) + if self.show_basis_vectors: + self.basis_vectors = self.get_basis_vectors( + i_hat_color=self.i_hat_color, + j_hat_color=self.j_hat_color, + ) + self.moving_vectors += list(self.basis_vectors) + self.i_hat, self.j_hat = self.basis_vectors + self.add(self.basis_vectors) + + def add_special_mobjects(self, mob_list, *mobs_to_add): + for mobject in mobs_to_add: + if mobject not in mob_list: + mob_list.append(mobject) + self.add(mobject) + + def add_background_mobject(self, *mobjects): + self.add_special_mobjects(self.background_mobjects, *mobjects) + + # TODO, this conflicts with Scene.add_fore + def add_foreground_mobject(self, *mobjects): + self.add_special_mobjects(self.foreground_mobjects, *mobjects) + + def add_transformable_mobject(self, *mobjects): + self.add_special_mobjects(self.transformable_mobjects, *mobjects) + + def add_moving_mobject(self, mobject, target_mobject=None): + mobject.target = target_mobject + self.add_special_mobjects(self.moving_mobjects, mobject) + + def get_unit_square(self, color=YELLOW, opacity=0.3, stroke_width=3): + square = self.square = Rectangle( + color=color, + width=self.plane.get_x_unit_size(), + height=self.plane.get_y_unit_size(), + stroke_color=color, + stroke_width=stroke_width, + fill_color=color, + fill_opacity=opacity + ) + square.move_to(self.plane.coords_to_point(0, 0), DL) + return square + + def add_unit_square(self, animate=False, **kwargs): + square = self.get_unit_square(**kwargs) + if animate: + self.play( + DrawBorderThenFill(square), + Animation(Group(*self.moving_vectors)) + ) + self.add_transformable_mobject(square) + self.bring_to_front(*self.moving_vectors) + self.square = square + return self + + def add_vector(self, vector, color=YELLOW, **kwargs): + vector = VectorScene.add_vector( + self, vector, color=color, **kwargs + ) + self.moving_vectors.append(vector) + return vector + + def write_vector_coordinates(self, vector, **kwargs): + coords = VectorScene.write_vector_coordinates(self, vector, **kwargs) + self.add_foreground_mobject(coords) + return coords + + def add_transformable_label( + self, vector, label, + transformation_name="L", + new_label=None, + **kwargs): + label_mob = self.label_vector(vector, label, **kwargs) + if new_label: + label_mob.target_text = new_label + else: + label_mob.target_text = "%s(%s)" % ( + transformation_name, + label_mob.get_tex() + ) + label_mob.vector = vector + label_mob.kwargs = kwargs + if "animate" in label_mob.kwargs: + label_mob.kwargs.pop("animate") + self.transformable_labels.append(label_mob) + return label_mob + + def add_title(self, title, scale_factor=1.5, animate=False): + if not isinstance(title, Mobject): + title = TexText(title).scale(scale_factor) + title.to_edge(UP) + title.add_background_rectangle() + if animate: + self.play(Write(title)) + self.add_foreground_mobject(title) + self.title = title + return self + + def get_matrix_transformation(self, matrix): + return self.get_transposed_matrix_transformation(np.array(matrix).T) + + def get_transposed_matrix_transformation(self, transposed_matrix): + transposed_matrix = np.array(transposed_matrix) + if transposed_matrix.shape == (2, 2): + new_matrix = np.identity(3) + new_matrix[:2, :2] = transposed_matrix + transposed_matrix = new_matrix + elif transposed_matrix.shape != (3, 3): + raise Exception("Matrix has bad dimensions") + return lambda point: np.dot(point, transposed_matrix) + + def get_piece_movement(self, pieces): + start = VGroup(*pieces) + target = VGroup(*[mob.target for mob in pieces]) + if self.leave_ghost_vectors: + self.add(start.copy().fade(0.7)) + return Transform(start, target, lag_ratio=0) + + def get_moving_mobject_movement(self, func): + for m in self.moving_mobjects: + if m.target is None: + m.target = m.copy() + target_point = func(m.get_center()) + m.target.move_to(target_point) + return self.get_piece_movement(self.moving_mobjects) + + def get_vector_movement(self, func): + for v in self.moving_vectors: + v.target = Vector(func(v.get_end()), color=v.get_color()) + norm = get_norm(v.target.get_end()) + if norm < 0.1: + v.target.get_tip().scale(norm) + return self.get_piece_movement(self.moving_vectors) + + def get_transformable_label_movement(self): + for l in self.transformable_labels: + l.target = self.get_vector_label( + l.vector.target, l.target_text, **l.kwargs + ) + return self.get_piece_movement(self.transformable_labels) + + def apply_matrix(self, matrix, **kwargs): + self.apply_transposed_matrix(np.array(matrix).T, **kwargs) + + def apply_inverse(self, matrix, **kwargs): + self.apply_matrix(np.linalg.inv(matrix), **kwargs) + + def apply_transposed_matrix(self, transposed_matrix, **kwargs): + func = self.get_transposed_matrix_transformation(transposed_matrix) + if "path_arc" not in kwargs: + net_rotation = np.mean([ + angle_of_vector(func(RIGHT)), + angle_of_vector(func(UP)) - np.pi / 2 + ]) + kwargs["path_arc"] = net_rotation + self.apply_function(func, **kwargs) + + def apply_inverse_transpose(self, t_matrix, **kwargs): + t_inv = np.linalg.inv(np.array(t_matrix).T).T + self.apply_transposed_matrix(t_inv, **kwargs) + + def apply_nonlinear_transformation(self, function, **kwargs): + self.plane.prepare_for_nonlinear_transform() + self.apply_function(function, **kwargs) + + def apply_function(self, function, added_anims=[], **kwargs): + if "run_time" not in kwargs: + kwargs["run_time"] = 3 + anims = [ + ApplyPointwiseFunction(function, t_mob) + for t_mob in self.transformable_mobjects + ] + [ + self.get_vector_movement(function), + self.get_transformable_label_movement(), + self.get_moving_mobject_movement(function), + ] + [ + Animation(f_mob) + for f_mob in self.foreground_mobjects + ] + added_anims + self.play(*anims, **kwargs)