Skip to content

Commit

Permalink
Fixing the behavior of .become to not modify target mobject via sid…
Browse files Browse the repository at this point in the history
…e effects fix color linking (#3508)

* Copied ndarray for rgbas when interpolating

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* changing .become to copy the target mobject

* change tests and test data to reflect .become new behavior

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update tests/test_graphical_units/test_mobjects.py

* removed unused copy_submobject kwarg

* added doctests and improved documentation

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <[email protected]>
  • Loading branch information
3 people authored Apr 1, 2024
1 parent 7a794e3 commit 909ffde
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 15 deletions.
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

0 comments on commit 909ffde

Please sign in to comment.