Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new animation TraceColor (Name for debate) and Graph.set_edge_color() #3533

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions manim/animation/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def __init__(
self.suspend_mobject_updating: bool = suspend_mobject_updating
self.lag_ratio: float = lag_ratio
self._on_finish: Callable[[Scene], None] = _on_finish
self.mobject: VMobject
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.mobject: VMobject

Is this neccessary?

if config["renderer"] == RendererType.OPENGL:
self.starting_mobject: OpenGLMobject = OpenGLMobject()
self.mobject: OpenGLMobject = (
Expand Down
55 changes: 55 additions & 0 deletions manim/animation/indication.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def construct(self):
"ApplyWave",
"Circumscribe",
"Wiggle",
"TraceColor",
]

from typing import Callable, Iterable, Optional, Tuple, Type, Union
Expand Down Expand Up @@ -319,6 +320,60 @@ def clean_up_from_scene(self, scene: Scene) -> None:
submob.pointwise_become_partial(start, 0, 1)


class TraceColor(ShowPartial):
"""Changing the color of a VMobject along its stroke.

Parameters
----------
mobject
The mobject whose stroke is animated.
color
The color to which the stroke is changed.

Examples
--------
.. manim:: TraceColorExample

class TraceColorExample(Scene):
def construct(self) -> None:
c = Circle().set_color(RED).set_opacity(0.5)
s = Square().set_color(BLUE).scale(2)
self.add(c)
self.play(TraceColor(c, color=BLUE, run_time=1), TraceColor(s, color=RED, run_time=2))
"""

def __init__(
self, mobject: "VMobject", color: ParsableManimColor, **kwargs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason VMobject is in quotations?

) -> None:
super().__init__(mobject, **kwargs)
self.color = color

def begin(self) -> None:
self.starting_mobject = (
self.mobject.copy()
) # Used to transition in the new line
self.mobject_copy = (
self.mobject.copy()
) # just kept as reference to what the mobject once was
self.inner_mobject = (
self.mobject.copy()
) # Used to transition out the original line
self.mobject.set_opacity(0)
self.mobject.add(self.inner_mobject)
self.mobject.add(self.starting_mobject)

def interpolate_mobject(self, alpha: float) -> None:
self.inner_mobject.pointwise_become_partial(self.mobject_copy, alpha, 1)
self.starting_mobject.pointwise_become_partial(
self.mobject_copy, 0, alpha
).set_color(self.color)

def finish(self) -> None:
self.mobject.remove(self.starting_mobject)
self.mobject.remove(self.inner_mobject)
self.mobject.become(self.mobject_copy.set_color(self.color))


class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
def __init__(self, vmobject, n_segments=10, time_width=0.1, remover=True, **kwargs):
self.n_segments = n_segments
Expand Down
73 changes: 72 additions & 1 deletion manim/mobject/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from __future__ import annotations

from manim.animation.indication import TraceColor
from manim.utils.color.core import ParsableManimColor

__all__ = [
"Graph",
"DiGraph",
Expand Down Expand Up @@ -584,7 +587,7 @@ def __init__(
edge_config: dict | None = None,
) -> None:
super().__init__()

self.edges = {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.edges = {}
self.edges: dict[tuple[Hashable, Hashable], Mobject] = {}

This way linters can raise errors about setting/getting stuff from this attribute.

nx_graph = self._empty_networkx_graph()
nx_graph.add_nodes_from(vertices)
nx_graph.add_edges_from(edges)
Expand Down Expand Up @@ -1109,6 +1112,20 @@ def add_edges(
)
return self.get_group_class()(*added_mobjects)

def get_edge(self, edge: tuple[Hashable, Hashable]) -> Mobject:
"""A dictionary of all edges in the graph.

The keys are tuples of vertex identifiers, and the values are
the corresponding edge mobjects.
"""
try:
return self.edges[edge]
except KeyError:
try:
return self.edges[(edge[1], edge[0])]
except KeyError:
raise ValueError(f"The {self} does not contain an edge '{edge}'")

@override_animate(add_edges)
def _add_edges_animation(self, *args, anim_args=None, **kwargs):
if anim_args is None:
Expand All @@ -1120,6 +1137,50 @@ def _add_edges_animation(self, *args, anim_args=None, **kwargs):
*(animation(mobj, **anim_args) for mobj in mobjects), group=self
)

def set_edge_color(
self, vertices: tuple[Hashable, Hashable], color: ParsableManimColor
):
"""Set the color of an edge. Can also be used to animate the color change in the direction of the edge.

Parameters
----------

vertices
The edge (as a tuple of vertex identifiers) whose color should be changed.

Returns
-------
Group
A group containing all newly added vertices and edges.

Examples
--------

.. manim:: SetEdgeColor

class SetEdgeColor(Scene):
def construct(self):
g = Graph(vertices=[1,2,3], edges=[(1,2), (2,3), (3,1)])
self.add(g)
self.play(AnimationGroup((g.animate.set_edge_color((u,v), RED) for u,v in g.edges), lag_ratio=0.5))
"""
self.get_edge(vertices).set_color(color)

@override_animate(set_edge_color)
def _set_edge_color_animation(self, vertices, color, anim_args=None, **kwargs):
if anim_args is None:
anim_args = {}
animation = anim_args.pop("animation", TraceColor)

mobj = self.get_edge(vertices) # Will error on DiGraph if edge is wrong way
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be a good idea to add a warning either in the code (via logger.warning) or in the docstring about DiGraph.
Same for the part below saying "this will not work on DiGraph"

if vertices in self.edges:
return animation(mobj, color=color, **anim_args)
else:
# Make sure we animate the edge in the right direction (i.e., from u to v) but also flip the name in edges if it is called inverted, this will not work on DiGraph
self.edges.pop((vertices[1], vertices[0]))
self.edges[vertices] = mobj
return animation(mobj.reverse_points(), color=color, **anim_args)

def _remove_edge(self, edge: tuple[Hashable]):
"""Remove an edge from the graph.

Expand Down Expand Up @@ -1771,5 +1832,15 @@ def update_edges(self, graph):
edge.become(new_edge)
edge.add_tip(tip)

def get_edge(self, edge: tuple[Hashable, Hashable]) -> Mobject:
"""Retrieves an edge by its vertices.

The order of the vertices is relevant here.
"""
try:
return self._edges[edge]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return self._edges[edge]
return self.edges[edge]

Typo?

except KeyError:
raise ValueError(f"The {self} does not contain an edge '{edge}'")

def __repr__(self: DiGraph) -> str:
return f"Directed graph on {len(self.vertices)} vertices and {len(self.edges)} edges"