Skip to content

Fixing the behavior of .become to not modify target mobject via side effects fix color linking #3508

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

Merged
merged 12 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 66 additions & 8 deletions manim/mobject/mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -2761,7 +2761,6 @@ def interpolate_color(self, mobject1: Mobject, mobject2: Mobject, alpha: float):
def become(
self,
mobject: Mobject,
copy_submobjects: bool = True,
match_height: bool = False,
match_width: bool = False,
match_depth: bool = False,
Expand All @@ -2774,20 +2773,25 @@ def become(
.. note::

If both match_height and match_width are ``True`` then the transformed :class:`~.Mobject`
will match the height first and then the width
will match the height first and then the width.

Parameters
----------
match_height
If ``True``, then the transformed :class:`~.Mobject` will match the height of the original
Whether or not to preserve the height of the original
:class:`~.Mobject`.
match_width
If ``True``, then the transformed :class:`~.Mobject` will match the width of the original
Whether or not to preserve the width of the original
:class:`~.Mobject`.
match_depth
If ``True``, then the transformed :class:`~.Mobject` will match the depth of the original
Whether or not to preserve the depth of the original
:class:`~.Mobject`.
match_center
If ``True``, then the transformed :class:`~.Mobject` will match the center of the original
Whether or not to preserve the center of the original
:class:`~.Mobject`.
stretch
If ``True``, then the transformed :class:`~.Mobject` will stretch to fit the proportions of the original
Whether or not to stretch the target mobject to match the
the proportions of the original :class:`~.Mobject`.

Examples
--------
Expand All @@ -2801,8 +2805,62 @@ def construct(self):
self.wait(0.5)
circ.become(square)
self.wait(0.5)
"""


The following examples illustrate how mobject measurements
change when using the ``match_...`` and ``stretch`` arguments.
We start with a rectangle that is 2 units high and 4 units wide,
which we want to turn into a circle of radius 3::

>>> from manim import Rectangle, Circle
>>> import numpy as np
>>> rect = Rectangle(height=2, width=4)
>>> circ = Circle(radius=3)

With ``stretch=True``, the target circle is deformed to match
the proportions of the rectangle, which results in the target
mobject being an ellipse with height 2 and width 4. We can
check that the resulting points satisfy the ellipse equation
:math:`x^2/a^2 + y^2/b^2 = 1` with :math:`a = 4/2` and :math:`b = 2/2`
being the semi-axes::

>>> result = rect.copy().become(circ, stretch=True)
>>> result.height, result.width
(2.0, 4.0)
>>> ellipse_eq = np.sum(result.get_anchors()**2 * [1/4, 1, 0], axis=1)
>>> np.allclose(ellipse_eq, 1)
True

With ``match_height=True`` and ``match_width=True`` the circle is
scaled such that the height or the width of the rectangle will
be preserved, respectively.
The points of the resulting mobject satisfy the circle equation
:math:`x^2 + y^2 = r^2` for the corresponding radius :math:`r`::

>>> result = rect.copy().become(circ, match_height=True)
>>> result.height, result.width
(2.0, 2.0)
>>> circle_eq = np.sum(result.get_anchors()**2, axis=1)
>>> np.allclose(circle_eq, 1)
True
>>> result = rect.copy().become(circ, match_width=True)
>>> result.height, result.width
(4.0, 4.0)
>>> circle_eq = np.sum(result.get_anchors()**2, axis=1)
>>> np.allclose(circle_eq, 2**2)
True

With ``match_center=True``, the resulting mobject is moved such that
its center is the same as the center of the original mobject::

>>> rect = rect.shift(np.array([0, 1, 0]))
>>> np.allclose(rect.get_center(), circ.get_center())
False
>>> result = rect.copy().become(circ, match_center=True)
>>> np.allclose(rect.get_center(), result.get_center())
True
"""
mobject = mobject.copy()
if stretch:
mobject.stretch_to_fit_height(self.height)
mobject.stretch_to_fit_width(self.width)
Expand Down
7 changes: 5 additions & 2 deletions manim/mobject/types/vectorized_mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ def set_stroke(
setattr(self, opacity_name, opacity)
if color is not None and background:
if isinstance(color, (list, tuple)):
self.background_stroke_color = color
self.background_stroke_color = ManimColor.parse(color)
else:
self.background_stroke_color = ManimColor(color)
return self
Expand Down Expand Up @@ -1738,7 +1738,10 @@ def interpolate_color(
interpolate(getattr(mobject1, attr), getattr(mobject2, attr), alpha),
)
if alpha == 1.0:
setattr(self, attr, getattr(mobject2, attr))
val = getattr(mobject2, attr)
if isinstance(val, np.ndarray):
val = val.copy()
setattr(self, attr, val)

def pointwise_become_partial(
self,
Expand Down
Binary file modified tests/test_graphical_units/control_data/mobjects/become.npz
Binary file not shown.
Binary file not shown.
21 changes: 16 additions & 5 deletions tests/test_graphical_units/test_mobjects.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,29 @@ def test_PointCloudDot(scene):
@frames_comparison
def test_become(scene):
s = Rectangle(width=2, height=1, color=RED).shift(UP)
d1, d2, d3 = (Dot() for _ in range(3))
d = Dot()

s1 = s.copy().become(d1, match_width=True).set_opacity(0.25).set_color(BLUE)
s1 = s.copy().become(d, match_width=True).set_opacity(0.25).set_color(BLUE)
s2 = (
s.copy()
.become(d2, match_height=True, match_center=True)
.become(d, match_height=True, match_center=True)
.set_opacity(0.25)
.set_color(GREEN)
)
s3 = s.copy().become(d3, stretch=True).set_opacity(0.25).set_color(YELLOW)
s3 = s.copy().become(d, stretch=True).set_opacity(0.25).set_color(YELLOW)

scene.add(s, d1, d2, d3, s1, s2, s3)
scene.add(s, d, s1, s2, s3)


@frames_comparison
def test_become_no_color_linking(scene):
a = Circle()
b = Square()
scene.add(a)
scene.add(b)
b.become(a)
b.shift(1 * RIGHT)
b.set_stroke(YELLOW, opacity=1)


@frames_comparison
Expand Down