diff --git a/active_projects/alt_calc.py b/active_projects/alt_calc.py new file mode 100644 index 00000000..3458b49a --- /dev/null +++ b/active_projects/alt_calc.py @@ -0,0 +1,73 @@ +from big_ol_pile_of_manim_imports import * + + +class NumberlineTransformationScene(Scene): + CONFIG = { + + } + + def setup(self): + pass + + +class ExampleNumberlineTransformationScene(NumberlineTransformationScene): + def construct(self): + pass + +# Scenes + + +class WriteOpeningWords(Scene): + def construct(self): + raw_string1 = "Dear calculus student," + raw_string2 = "You're about to go through your first course. Like " + \ + "any new topic, it will take some hard work to understand," + words1, words2 = [ + TextMobject("\\Large", *rs.split(" ")) + for rs in raw_string1, raw_string2 + ] + words1.next_to(words2, UP, aligned_edge=LEFT, buff=LARGE_BUFF) + words = VGroup(*it.chain(words1, words2)) + words.scale_to_fit_width(FRAME_WIDTH - 2 * LARGE_BUFF) + words.to_edge(UP) + + letter_wait = 0.05 + word_wait = 2 * letter_wait + comma_wait = 5 * letter_wait + for word in words: + self.play(LaggedStart( + FadeIn, word, + run_time=len(word) * letter_wait, + lag_ratio=1.5 / len(word) + )) + self.wait(word_wait) + if word.get_tex_string()[-1] == ",": + self.wait(comma_wait) + + +class StartingCalc101(PiCreatureScene): + CONFIG = { + # "default_pi_creature_kwargs": { + # "color": BLUE, + # "flip_at_start": False, + # }, + } + + def construct(self): + randy = self.pi_creature + deriv_string = "\\frac{df}{dx}(x) = \\lim(\\delta x \\to \\infty)" + \ + "{f(x + \\delta x) - f(x) \\over \\delta x}" + equations = VGroup( + TexMobject(*break_up_string_by_terms(deriv_string, "\\delta x")) + ) + title = TextMobject("Calculus 101") + title.to_edge(UP) + h_line = Line(LEFT, RIGHT) + h_line.scale_to_fit_width(FRAME_WIDTH - LARGE_BUFF) + h_line.next_to(title, DOWN) + + self.add(title, h_line) + self.play(randy.change, "erm", title) + self.wait() + + diff --git a/active_projects/eola2/cramer.py b/active_projects/eola2/cramer.py index 7dd9d951..69eb2d0a 100644 --- a/active_projects/eola2/cramer.py +++ b/active_projects/eola2/cramer.py @@ -2154,7 +2154,7 @@ class Thumbnail(TransformingAreasYCoord): words = TextMobject("Cramer's", "rule") words.scale_to_fit_width(7) - # words.add_background_rectangle_to_parts() + # words.add_background_rectangle_to_submobjects() words.add_background_rectangle() words.to_edge(UP) self.add(words) diff --git a/active_projects/eop/chapter1/prob_dist_visuals.py b/active_projects/eop/chapter1/prob_dist_visuals.py index b72957e1..6bd2dbae 100644 --- a/active_projects/eop/chapter1/prob_dist_visuals.py +++ b/active_projects/eop/chapter1/prob_dist_visuals.py @@ -58,7 +58,7 @@ class ProbabilityDistributions(PiCreatureScene): p_rain_whole_label.next_to(brace_rain, UP) brace_sun = Brace(sun_rect, DOWN) - p_sun_label = TextMobject("$P($sun$)=$").scale(text_scale) + p_sun_label = TextMobject("$P($sunshine$)=$").scale(text_scale) p_sun_decimal = DecimalNumber(p_sun).scale(text_scale) p_sun_decimal.next_to(p_sun_label) p_sun_whole_label = VGroup(p_sun_label, p_sun_decimal) @@ -97,7 +97,7 @@ class ProbabilityDistributions(PiCreatureScene): new_brace_sun = Brace(new_sun_rect, DOWN) - new_p_sun_label = TextMobject("$P($sun$)=$").scale(text_scale) + new_p_sun_label = TextMobject("$P($sunshine$)=$").scale(text_scale) new_p_sun_decimal = DecimalNumber(new_p_sun).scale(text_scale) new_p_sun_decimal.next_to(new_p_sun_label) new_p_sun_whole_label = VGroup(new_p_sun_label, new_p_sun_decimal) @@ -147,20 +147,28 @@ class ProbabilityDistributions(PiCreatureScene): self.play(MoveToTarget(forecast)) + self.play( + FadeOut(brace_rain), + FadeOut(brace_sun), + FadeOut(p_rain_whole_label), + FadeOut(p_sun_whole_label), + ) + + # COIN FLIP - coin_flip_rect = BrickRow(3) + coin_flip_rect = BrickRow(3, height = 2, width = 10) for (i, brick) in enumerate(coin_flip_rect.rects): tally = TallyStack(3 - i, i) - tally.next_to(brick, UP) + tally.move_to(brick) coin_flip_rect.add(tally) - coin_flip_rect.scale(0.7) + coin_flip_rect.scale(0.8).shift(2*RIGHT) self.play(FadeIn(coin_flip_rect)) counts = [1, 3, 3, 1] @@ -180,75 +188,72 @@ class ProbabilityDistributions(PiCreatureScene): coin_flip_rect.add(braces, labels) - self.play(coin_flip_rect.to_corner,UR) - + coin_flip_rect.target = coin_flip_rect.copy().scale(0.6) + coin_flip_rect.target.to_corner(UR, buff = LARGE_BUFF) + self.play( + MoveToTarget(coin_flip_rect) + ) + self.play( + FadeOut(braces), + FadeOut(labels) + ) # DOUBLE DICE THROW cell_size = 0.5 dice_table = TwoDiceTable(cell_size = cell_size, label_scale = 0.7) - dice_table.shift(DOWN) + dice_table.shift(0.8 * DOWN) + dice_unit_rect = SurroundingRectangle(dice_table.cells, buff = 0, + stroke_color = WHITE) - self.play(FadeIn(dice_table)) - self.wait() - self.play( - FadeOut(dice_table.rows), - FadeOut(dice_table.labels), - dice_table.cells.fade, 0.8 - ) - - dice_table_braces = VGroup() - dice_table_probs = VGroup() dice_table_grouped_cells = VGroup() for i in range(6): cell = dice_table.cells[6 * i] start = cell.get_center() - color = cell.get_fill_color() - brace = Brace(cell, LEFT, buff = 0, color = color) - brace.stretch(0.5,0) stop = start + cell_size * LEFT + cell_size * DOWN - p_label = TexMobject("{" + str(i + 1) + "\over 36}", color = color) - p_label.scale(0.35) - p_label.next_to(brace, LEFT) - dice_table_probs.add(p_label) - dice_table_braces.add(brace) - + dice_table_grouped_cells.add(VGroup(*[ dice_table.cells[6 * i - 5 * k] for k in range(i + 1) ])) - for i in range(5): cell = dice_table.cells[31 + i] start = cell.get_center() - color = cell.get_fill_color() - brace = Brace(cell, DOWN, buff = 0, color = color) - brace.stretch(0.5, 1) stop = start + cell_size * LEFT + cell_size * DOWN - p_label = TexMobject("{" + str(5 - i) + "\over 36}", color = color) - p_label.scale(0.35) - p_label.next_to(brace, DOWN) - dice_table_probs.add(p_label) - dice_table_braces.add(brace) - + dice_table_grouped_cells.add(VGroup(*[ dice_table.cells[31 + i - 5 * k] for k in range(5 - i) ])) + self.play( + FadeIn(dice_unit_rect), + FadeIn(dice_table.rows) + ) - # group the dice table cells to make them appear in the right order + for (cell, label) in zip(dice_table.cells, dice_table.labels): + cell.add(label) self.play( - LaggedStart(ShowCreation, dice_table_braces, lag_ratio = lag_ratio, run_time = run_time), - LaggedStart(Write, dice_table_probs, lag_ratio = lag_ratio, run_time = run_time), - LaggedStart(ApplyMethod, dice_table_grouped_cells, arg_creator = - lambda m : (m.fade, -4), lag_ratio = lag_ratio, run_time = run_time - ) + LaggedStart(FadeIn, dice_table_grouped_cells, + lag_ratio = lag_ratio, run_time = run_time) + ) + + self.wait() + self.play( + FadeOut(dice_table.rows), + FadeOut(dice_unit_rect), + ) + + + self.play( + dice_table_grouped_cells.space_out_submobjects, {"factor" : 1.9}, + rate_func=there_and_back_with_pause, + run_time=run_time ) @@ -269,5 +274,6 @@ class ProbabilityDistributions(PiCreatureScene): + diff --git a/animation/indication.py b/animation/indication.py index 5a6f1d4e..c22bdd91 100644 --- a/animation/indication.py +++ b/animation/indication.py @@ -6,14 +6,21 @@ from constants import * from animation.animation import Animation from animation.movement import Homotopy +from animation.composition import AnimationGroup +from animation.composition import Succession +from animation.creation import ShowCreation from animation.creation import ShowPartial +from animation.creation import FadeOut from animation.transform import Transform from mobject.mobject import Mobject from mobject.geometry import Circle from mobject.geometry import Dot +from mobject.shape_matchers import SurroundingRectangle +from utils.bezier import interpolate from utils.config_ops import digest_config from utils.rate_functions import squish_rate_func from utils.rate_functions import there_and_back +from utils.rate_functions import wiggle class FocusOn(Transform): @@ -93,14 +100,49 @@ class ShowCreationThenDestruction(ShowPassingFlash): } +class AnimationOnSurroundingRectangle(AnimationGroup): + CONFIG = { + "surrounding_rectangle_config": {}, + # Function which takes in a rectangle, and spits + # out some animation. Could be some animation class, + # could be something more + "rect_to_animation": Animation + } + + def __init__(self, mobject, **kwargs): + digest_config(self, kwargs) + rect = SurroundingRectangle( + mobject, **self.surrounding_rectangle_config + ) + AnimationGroup.__init__(self, self.rect_to_animation(rect, **kwargs)) + + +class ShowPassingFlashAround(AnimationOnSurroundingRectangle): + CONFIG = { + "rect_to_animation": ShowPassingFlash + } + + +class ShowCreationThenDestructionAround(AnimationOnSurroundingRectangle): + CONFIG = { + "rect_to_animation": ShowCreationThenDestruction + } + + +class CircleThenFadeAround(AnimationOnSurroundingRectangle): + CONFIG = { + "rect_to_animation": lambda rect: Succession( + ShowCreation, rect, + FadeOut, rect, + ) + } + + class ApplyWave(Homotopy): CONFIG = { - "direction": DOWN, + "direction": UP, "amplitude": 0.2, "run_time": 1, - "apply_function_kwargs": { - "maintain_smoothness": False, - }, } def __init__(self, mobject, **kwargs): @@ -110,9 +152,9 @@ class ApplyWave(Homotopy): vect = self.amplitude * self.direction def homotopy(x, y, z, t): - start_point = np.array([x, y, z]) alpha = (x - left_x) / (right_x - left_x) - power = np.exp(2 * (alpha - 0.5)) + # lf = self.lag_factor + power = np.exp(2.0 * (alpha - 0.5)) nudge = there_and_back(t**power) return np.array([x, y, z]) + nudge * vect Homotopy.__init__(self, homotopy, mobject, **kwargs) @@ -195,7 +237,6 @@ class TurnInsideOut(Transform): } def __init__(self, mobject, **kwargs): - mobject.sort_points(np.linalg.norm) mob_copy = mobject.copy() - mob_copy.sort_points(lambda p: -np.linalg.norm(p)) + mob_copy.reverse_points() Transform.__init__(self, mobject, mob_copy, **kwargs) diff --git a/for_3b1b_videos/pi_creature_scene.py b/for_3b1b_videos/pi_creature_scene.py index c9ce461e..b9ddaac0 100644 --- a/for_3b1b_videos/pi_creature_scene.py +++ b/for_3b1b_videos/pi_creature_scene.py @@ -32,10 +32,10 @@ class PiCreatureScene(Scene): "seconds_to_blink": 3, "pi_creatures_start_on_screen": True, "default_pi_creature_kwargs": { - "color": GREY_BROWN, - "flip_at_start": True, + "color": BLUE, + "flip_at_start": False, }, - "default_pi_creature_start_corner": DOWN + LEFT, + "default_pi_creature_start_corner": DL, } def setup(self): diff --git a/mobject/matrix.py b/mobject/matrix.py index 924b9917..727f8f4d 100644 --- a/mobject/matrix.py +++ b/mobject/matrix.py @@ -116,10 +116,6 @@ class Matrix(VMobject): self.brackets = VGroup(l_bracket, r_bracket) return self - def add_background_rectangle(self): - self.background_rectangle = BackgroundRectangle(self) - self.add_to_back(self.background_rectangle) - def set_color_columns(self, *colors): for i, color in enumerate(colors): VGroup(*self.mob_matrix[:, i]).set_color(color) diff --git a/mobject/mobject.py b/mobject/mobject.py index 235ed515..72cfd33a 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import copy import itertools as it import numpy as np @@ -28,7 +30,6 @@ class Mobject(Container): """ CONFIG = { "color": WHITE, - "stroke_width": DEFAULT_POINT_THICKNESS, "name": None, "dim": 3, "target": None, @@ -464,6 +465,27 @@ class Mobject(Container): self.shift(start - self.points[0]) return self + # Background rectangle + def add_background_rectangle(self, color=BLACK, opacity=0.75, **kwargs): + from mobject.shape_matchers import BackgroundRectangle + self.background_rectangle = BackgroundRectangle( + self, color=color, + fill_opacity=opacity, + **kwargs + ) + self.add_to_back(self.background_rectangle) + return self + + def add_background_rectangle_to_submobjects(self, **kwargs): + for submobject in self.submobjects: + submobject.add_background_rectangle(**kwargs) + return self + + def add_background_rectangle_to_family_members_with_points(self, **kwargs): + for mob in self.family_members_with_points(): + mob.add_background_rectangle(**kwargs) + return self + # Match other mobvject properties def match_color(self, mobject): @@ -643,6 +665,7 @@ class Mobject(Container): return result # Pseudonyms for more general get_critical_point method + def get_edge_center(self, direction): return self.get_critical_point(direction) @@ -719,7 +742,8 @@ class Mobject(Container): def submobject_family(self): sub_families = map(Mobject.submobject_family, self.submobjects) - all_mobjects = [self] + list(it.chain(*sub_families)) + # all_mobjects = [self] + list(it.chain(*sub_families)) + all_mobjects = list(it.chain(*sub_families)) + [self] return remove_list_redundancies(all_mobjects) def family_members_with_points(self): diff --git a/mobject/numbers.py b/mobject/numbers.py index 118289f7..5a6ff682 100644 --- a/mobject/numbers.py +++ b/mobject/numbers.py @@ -3,9 +3,7 @@ from __future__ import absolute_import from constants import * from mobject.svg.tex_mobject import TexMobject -from mobject.types.vectorized_mobject import VGroup from mobject.types.vectorized_mobject import VMobject -from mobject.shape_matchers import BackgroundRectangle class DecimalNumber(VMobject): @@ -51,7 +49,7 @@ class DecimalNumber(VMobject): ) if self.unit is not None: - self.unit_sign = TexMobject(self.unit, color = self.color) + self.unit_sign = TexMobject(self.unit, color=self.color) self.add(self.unit_sign) self.arrange_submobjects( @@ -70,16 +68,6 @@ class DecimalNumber(VMobject): if self.include_background_rectangle: self.add_background_rectangle() - def add_background_rectangle(self): - # TODO, is this the best way to handle - # background rectangles? - self.background_rectangle = BackgroundRectangle(self) - self.submobjects = [ - self.background_rectangle, - VGroup(*self.submobjects) - ] - return self - class Integer(DecimalNumber): CONFIG = { diff --git a/mobject/svg/tex_mobject.py b/mobject/svg/tex_mobject.py index cbb57a72..06c6df13 100644 --- a/mobject/svg/tex_mobject.py +++ b/mobject/svg/tex_mobject.py @@ -2,14 +2,16 @@ from constants import * from svg_mobject import SVGMobject from svg_mobject import VMobjectFromSVGPathstring -from mobject.shape_matchers import BackgroundRectangle from utils.config_ops import digest_config +from utils.strings import split_string_list_to_isolate_substring from mobject.types.vectorized_mobject import VGroup -from mobject.types.vectorized_mobject import VMobject from mobject.types.vectorized_mobject import VectorizedPoint import operator as op +# TODO list +# - Make sure if "color" is passed into TexMobject, it behaves as expected + TEX_MOB_SCALE_FACTOR = 0.05 @@ -31,57 +33,36 @@ class TexSymbol(VMobjectFromSVGPathstring): self.set_fill(opacity=opacity) -class TexMobject(SVGMobject): +class SingleStringTexMobject(SVGMobject): CONFIG = { "template_tex_file": TEMPLATE_TEX_FILE, "stroke_width": 0, "fill_opacity": 1.0, - "fill_color": WHITE, "should_center": True, - "arg_separator": " ", "height": None, "organize_left_to_right": False, "propagate_style_to_family": True, "alignment": "", } - def __init__(self, *args, **kwargs): - digest_config(self, kwargs, locals()) - - if "color" in kwargs.keys() and "fill_color" not in kwargs.keys(): - self.fill_color = kwargs["color"] - - # TODO, Eventually remove this - if len(args) == 1 and isinstance(args[0], list): - self.args = args[0] - ## - assert(all([isinstance(a, str) for a in self.args])) - self.tex_string = self.get_modified_expression() + def __init__(self, tex_string, **kwargs): + digest_config(self, kwargs) + assert(isinstance(tex_string, str)) + self.tex_string = tex_string file_name = tex_to_svg_file( - self.tex_string, + self.get_modified_expression(tex_string), self.template_tex_file ) SVGMobject.__init__(self, file_name=file_name, **kwargs) - self.scale(TEX_MOB_SCALE_FACTOR) + if self.height is None: + self.scale(TEX_MOB_SCALE_FACTOR) if self.organize_left_to_right: self.organize_submobjects_left_to_right() - def path_string_to_mobject(self, path_string): - # Overwrite superclass default to use - # specialized path_string mobject - return TexSymbol(path_string) - - def generate_points(self): - SVGMobject.generate_points(self) - if len(self.args) > 1: - self.handle_multiple_args() - - def get_modified_expression(self): - result = self.arg_separator.join(self.args) - result = " ".join([self.alignment, result]) + def get_modified_expression(self, tex_string): + result = self.alignment + " " + tex_string result = result.strip() result = self.modify_special_strings(result) - return result def modify_special_strings(self, tex): @@ -123,27 +104,67 @@ class TexMobject(SVGMobject): def get_tex_string(self): return self.tex_string - def handle_multiple_args(self): + def path_string_to_mobject(self, path_string): + # Overwrite superclass default to use + # specialized path_string mobject + return TexSymbol(path_string) + + def organize_submobjects_left_to_right(self): + self.sort_submobjects(lambda p: p[0]) + return self + + +class TexMobject(SingleStringTexMobject): + CONFIG = { + "arg_separator": " ", + "substrings_to_isolate": [], + "tex_to_color_map": {}, + } + + def __init__(self, *tex_strings, **kwargs): + digest_config(self, kwargs) + tex_strings = self.break_up_tex_strings(tex_strings) + self.tex_strings = tex_strings + SingleStringTexMobject.__init__( + self, self.arg_separator.join(tex_strings), **kwargs + ) + self.break_up_by_substrings() + self.set_color_by_tex_to_color_map(self.tex_to_color_map) + + if self.organize_left_to_right: + self.organize_submobjects_left_to_right() + + def break_up_tex_strings(self, tex_strings): + substrings_to_isolate = op.add( + self.substrings_to_isolate, + self.tex_to_color_map.keys() + ) + split_list = split_string_list_to_isolate_substring( + tex_strings, *substrings_to_isolate + ) + split_list = map(str.strip, split_list) + split_list = filter(lambda s: s != '', split_list) + return split_list + + def break_up_by_substrings(self): """ Reorganize existing submojects one layer - deeper based on the structure of args (as a list of strings) + deeper based on the structure of tex_strings (as a list + of tex_strings) """ new_submobjects = [] curr_index = 0 - self.expression_parts = list(self.args) - for expr in self.args: - sub_tex_mob = TexMobject(expr, **self.CONFIG) - sub_tex_mob.tex_string = expr # Want it unmodified + for tex_string in self.tex_strings: + sub_tex_mob = SingleStringTexMobject(tex_string, **self.CONFIG) num_submobs = len(sub_tex_mob.submobjects) new_index = curr_index + num_submobs if num_submobs == 0: - if len(self) > curr_index: - last_submob_index = curr_index - else: - last_submob_index = -1 - sub_tex_mob.submobjects = [VectorizedPoint( - self.submobjects[last_submob_index].get_right() - )] + # For cases like empty tex_strings, we want the corresponing + # part of the whole TexMobject to be a VectorizedPoint + # positioned in the right part of the TexMobject + sub_tex_mob.submobjects = [VectorizedPoint()] + last_submob_index = min(curr_index, len(self.submobjects) - 1) + sub_tex_mob.move_to(self.submobjects[last_submob_index], RIGHT) else: sub_tex_mob.submobjects = self.submobjects[curr_index:new_index] new_submobjects.append(sub_tex_mob) @@ -161,15 +182,9 @@ class TexMobject(SVGMobject): else: return tex1 == tex2 - tex_submobjects = filter( - lambda m: isinstance(m, TexMobject), - self.submobject_family() - ) - if hasattr(self, "expression_parts"): - tex_submobjects.remove(self) return VGroup(*filter( lambda m: test(tex, m.get_tex_string()), - tex_submobjects + self.submobjects )) def get_part_by_tex(self, tex, **kwargs): @@ -185,7 +200,7 @@ class TexMobject(SVGMobject): def set_color_by_tex_to_color_map(self, texs_to_color_map, **kwargs): for texs, color in texs_to_color_map.items(): try: - # If the given key behaves like strings + # If the given key behaves like tex_strings texs + '' self.set_color_by_tex(texs, color, **kwargs) except TypeError: @@ -204,32 +219,13 @@ class TexMobject(SVGMobject): part = self.get_part_by_tex(tex, **kwargs) return self.index_of_part(part) - def organize_submobjects_left_to_right(self): - self.sort_submobjects(lambda p: p[0]) - return self - - def sort_submobjects_alphabetically(self): - def alphabetical_cmp(m1, m2): - if not all([isinstance(m, TexMobject) for m in m1, m2]): - return 0 - return cmp(m1.get_tex_string(), m2.get_tex_string()) - self.submobjects.sort(alphabetical_cmp) - return self - - def add_background_rectangle(self, color=BLACK, opacity=0.75, **kwargs): - self.background_rectangle = BackgroundRectangle( - self, color=color, - fill_opacity=opacity, - **kwargs - ) - letters = VMobject(*self.submobjects) - self.submobjects = [self.background_rectangle, letters] - return self - - def add_background_rectangle_to_parts(self): - for part in self: - part.add_background_rectangle() - return self + def split(self): + # Many old scenes assume that when you pass in a single string + # to TexMobject, it indexes across the characters. + if len(self.submobjects) == 1: + return self.submobjects[0].split() + else: + return super(TexMobject, self).split() class TextMobject(TexMobject): diff --git a/mobject/types/point_cloud_mobject.py b/mobject/types/point_cloud_mobject.py index b2b9863a..7b9d329d 100644 --- a/mobject/types/point_cloud_mobject.py +++ b/mobject/types/point_cloud_mobject.py @@ -12,6 +12,10 @@ from utils.iterables import stretch_array_to_length class PMobject(Mobject): + CONFIG = { + "stroke_width": DEFAULT_POINT_THICKNESS, + } + def init_points(self): self.rgbas = np.zeros((0, 4)) self.points = np.zeros((0, 3)) diff --git a/mobject/types/vectorized_mobject.py b/mobject/types/vectorized_mobject.py index 92ddf402..1290bc78 100644 --- a/mobject/types/vectorized_mobject.py +++ b/mobject/types/vectorized_mobject.py @@ -18,6 +18,7 @@ class VMobject(Mobject): "fill_color": None, "fill_opacity": 0.0, "stroke_color": None, + "stroke_width": DEFAULT_POINT_THICKNESS, # Indicates that it will not be displayed, but # that it should count in parent mobject's path "is_subpath": False, diff --git a/old_projects/wallis.py b/old_projects/wallis.py index be7efe7d..b2ffb1ed 100644 --- a/old_projects/wallis.py +++ b/old_projects/wallis.py @@ -2135,7 +2135,7 @@ class PlugObserverIntoPolynomial(DistanceProductScene): ) question.scale(text_scale_val) question.next_to(dot, RIGHT) - question.add_background_rectangle_to_parts() + question.add_background_rectangle_to_submobjects() f_words = TextMobject("$f$", "of the way") third_words = TextMobject("$\\frac{1}{3}$", "of the way") diff --git a/utils/strings.py b/utils/strings.py index fdadf99b..67ed144c 100644 --- a/utils/strings.py +++ b/utils/strings.py @@ -1,5 +1,6 @@ import re import string +import itertools as it def to_camel_case(name): @@ -24,3 +25,37 @@ def camel_case_initials(name): def complex_string(complex_num): return filter(lambda c: c not in "()", str(complex_num)) + + +def split_string_to_isolate_substrings(full_string, *substrings_to_isolate): + """ + Given a string, and an arbitrary number of possible substrings, returns a list + of strings which would concatenate to make the full string, and in which + these special substrings appear as their own elements. + + For example, split_string_to_isolate_substrings("to be or not to be", "to", "be") would + return ["to", " ", "be", " or not ", "to", " ", "be"] + """ + if len(substrings_to_isolate) == 0: + return [full_string] + substring_to_isolate = substrings_to_isolate[0] + all_substrings = list(it.chain(*zip( + full_string.split(substring_to_isolate), + it.repeat(substring_to_isolate) + ))) + all_substrings.pop(-1) + all_substrings = filter(lambda s: s != "", all_substrings) + return split_string_list_to_isolate_substring( + all_substrings, *substrings_to_isolate[1:] + ) + + +def split_string_list_to_isolate_substring(string_list, *substrings_to_isolate): + """ + Similar to split_string_to_isolate_substrings, but the first argument + is a list of strings, thought of as something already broken up a bit. + """ + return list(it.chain(*[ + split_string_to_isolate_substrings(s, *substrings_to_isolate) + for s in string_list + ]))