Skip to content

Instantly share code, notes, and snippets.

@abul4fia
Last active May 15, 2024 17:49
Show Gist options
  • Save abul4fia/3bbe8e0c1d19a007cad035bb8be90387 to your computer and use it in GitHub Desktop.
Save abul4fia/3bbe8e0c1d19a007cad035bb8be90387 to your computer and use it in GitHub Desktop.
Finding sub-texts by shape in manim

Finding shapes in Tex mobjects

This module implements some utility functions to search some shapes (formulas, symbols) inside a Tex (or MathTex) mobject.

The approach is to compare the shapes (bezier curves) of the symbols, instead of the particular strings that produced them. This approach is generic and avoids the use of other manim mechanisms that would break under some circunstances, such as the presence of \frac{}{} in a formula.

The function you would probably want to use is group_shapes_in_tex(). Despite the confusing name, the goal is to find all the occurences of a given symbol (or group of symbols, or list of symbols) inside a Tex mobject, and return a VGroup containing them all. This way you can apply whatever operations you wan to the resulting group, such as changing the color, iterate on its members to highligth them, etc.

The remaining functions in this module are auxiliary or utility functions such as the following (for detais about parameters and usage, see the docstrings in the code)

  • search_shape_in_text() which searches a single shape in the given text, and returns a list of slice() objects containing the start and end indexes of the submobjects that matched (it is a list because it can be several matches, as for example if you search for the "x" shape in an equation in which "x" appears several times)

  • search_shapes_in_tex() which receives a list of shapes instead a single one, and returns the list of all matches of any of the given shapes. For example you can pass a list containing the shape of an "a" and the shape of an "A", and will return a list with all the slices in which either "a" or "A" appear

  • group_shapes_in_text() receives either a single shape or a list of shapes, and uses the above functions to locate the required indexes. Then build and returns a VGroup containing the selected submobjects.

  • all_sizes_symbol() generates all the shapes of a given symbol, in different sizes. This is useful if you want to locate for example all "x" shapes in a formula, no matter if they appear in subindices, exponents, sum limits, etc. LaTeX uses a slightly different font for each case, so in order to find all of them you have to use this function to generate the list of all those slightly different shapes

  • colorize_similar_tex() this receives a "config" dict with strings as keys and colors and values. It uses MathTex() convert each key (string) in a shape, searches the shape in the given mobject, and changes its color to the value given in the dict.

def search_shape_in_text(text:VMobject, shape:VMobject, index=0, threshold=None):
"""Receives two VMobjects resulting from rendering text (either by Tex, Text
or MathTex) and looks for occurrences of the second in the first, but comparing
the shapes and not the text itself.
In essence, it goes through all the elements of text[index] grouped according to the
number of elements of shape[0], and for each group it calculates a hash of
both shapes and compares them.
It returns a list with all the indices of text[index] where it was found. Each
element of that list is a slice because the text may span more than one
element of text[0].
The parameter threshold influences the result. With its default value of None
the matching of shapes can produce some false positives (but this is how manim's
TransformMatchingShapes works too). You can give it very large values (above 100000)
to reduce the chance of false positives, at the cost of a little longer run time.
Example (changing the color of all x's):
gx = MathTex(r'''
g(x) = \begin{cases}
x(2-x) &(|x-1| \leq 1) \\
0 &(|x-1| > 1)
\end{cases}''')
self.add(gx)
self.wait()
self.play(*[
Transform(gx[0][group], MathTex("a").move_to(gx[0][group]), path_arc=-PI)
for group in search_shape_in_text(gx, MathTex("x"))
])
self.wait()
"""
def get_mobject_key(mobject: Mobject) -> int:
mobject.save_state().center().scale_to_fit_height(1)
r = np.array2string(mobject.points, precision=2,
separator=' ', suppress_small=True, threshold=threshold)
r = r.replace('-0. ', ' 0. ')
mobject.restore()
return hash(r)
results = []
l = len(shape.submobjects[0])
shape_aux = VMobject()
shape_aux.points = np.concatenate([p.points for p in shape.submobjects[0]])
for i in range(len(text.submobjects[index])-l+1):
subtext = VMobject()
subtext.points = np.concatenate([p.points for p in text.submobjects[index][i:i+l]])
if get_mobject_key(subtext) == get_mobject_key(shape_aux):
results.append(slice(i, i+l))
return results
def search_shapes_in_text(text:VMobject, shapes:list[VMobject], index=0):
"""Like the previous one, but receives a list of possible sub-texts to search for.
Example (replaces all x's, both normal and small ones,
which have a different shape):
gx = MathTex(r'''
\sum_{x=0}^\infty \frac{1}{x!} = e^x
''').scale(3)
self.add(gx)
self.wait()
self.play(*[
gx[0][group].animate.set_color(YELLOW)
for group in search_shapes_in_text(gx, [MathTex("x"), MathTex("^x")])
])
self.wait()
"""
results = []
for shape in shapes:
results += search_shape_in_text(text, shape, index)
return results
def group_shapes_in_text(text: VMobject, shapes: VMobject | list[VMobject], index = 0):
"""
This functions receives a text in which it has to search a given shape (or list of shapes)
It returns a VGroup with the shapes found in the text
It is a usability improvement with respect to search_shape_in_text, because it directly returns
a group of VMobjects instead of index slices. It also accepts a list or a single shape.
Example of use (replaces all x's, both normal and small ones,
which have a different shape):
gx = MathTex(r'''
\sum_{x=0}^\infty \frac{1}{x!} = e^x
''').scale(3)
self.add(gx)
self.wait()
results = group_shapes_in_text(gx, [MathTex("x"), MathTex("^x")])
self.play(results.animate.set_color(YELLOW))
self.wait()
"""
if isinstance(shapes, VMobject):
shapes = [shapes]
results = search_shapes_in_text(text, shapes, index)
return VGroup(*[text[index][s] for s in results])
def colorize_similar_tex(eq, config, index=0):
# Small adition (inspired by VirusMinus8 discord user
"""
This function receives a Tex or MathTex object and a dictionary. The dictionary keys
are strings and the values are colors. The strings are rendered through MathTex
and the resulting shape is searched in eq. All occurences are changed to the given
color. If an index is provided, the search happens only inside `eq[index]`, for the
case it has several submobject. index=0 searchs all the submobjects.
"""
for key, color in config.items():
group_shapes_in_text(eq, MathTex(key), index).set_color(color)
return eq
def all_sizes_symbol(txt: str):
"""Builds a list of Tex objects with the same symbol in different sizes,
which can be useful to find the symbol in a formula, no matter if it appears
as a subscript, superscript, etc.
Example of use:
g = group_shapes_in_text(eq, all_sizes_symbol("x+1"))
g.set_color(RED)
this will make red all appearances of "x+1", at any size, in the formula eq
"""
sizes = [r"\displaystyle", r"\textstyle", r"\scriptstyle", r"\scriptscriptstyle"]
return [Tex(f"${size} {txt}$") for size in sizes]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment