I took the example from /en/stable/reference/manim.animation.transform.ReplacementTransform.html and changed it a bit (I added more numbers):
class ReplacementTransformOrTransform(Scene):
def construct(self):
# set up the numbers
r_transform = VGroup(*[Integer(i) for i in range(1, 5)])
text_1 = Text("ReplacementTransform", color=RED)
r_transform.add(text_1)
transform = VGroup(*[Integer(i) for i in range(5, 9)])
text_2 = Text("Transform", color=BLUE)
transform.add(text_2)
ints = VGroup(r_transform, transform)
texts = VGroup(text_1, text_2).scale(0.75)
r_transform.arrange(direction=UP, buff=1)
transform.arrange(direction=UP, buff=1)
ints.arrange(buff=2)
self.add(ints, texts)
# The mobs replace each other and none are left behind
self.play(ReplacementTransform(r_transform[0], r_transform[1]))
self.play(ReplacementTransform(r_transform[1], r_transform[2]))
self.play(ReplacementTransform(r_transform[2], r_transform[3]))
# The mobs linger after the Transform()
self.play(Transform(transform[0], transform[1]))
self.play(Transform(transform[1], transform[2]))
self.play(Transform(transform[2], transform[3]))
self.wait()
When I run it, this happens:
- The 1 goes to 2, leaving nothing behind.
- The 2 goes to 3, leaving nothing behind.
- The 3 goes to 4, leaving another 3 behind (just like a regular
Transform()
).
Why? And more importantly: how do I move the 3 without leaving anything behind?
NOTE: I have a fresh reinstall with python 3.13.1 and manim community 0.19.0, they are the latest versions.
I took the example from https://docs.manim.community/en/stable/reference/manim.animation.transform.ReplacementTransform.html and changed it a bit (I added more numbers):
class ReplacementTransformOrTransform(Scene):
def construct(self):
# set up the numbers
r_transform = VGroup(*[Integer(i) for i in range(1, 5)])
text_1 = Text("ReplacementTransform", color=RED)
r_transform.add(text_1)
transform = VGroup(*[Integer(i) for i in range(5, 9)])
text_2 = Text("Transform", color=BLUE)
transform.add(text_2)
ints = VGroup(r_transform, transform)
texts = VGroup(text_1, text_2).scale(0.75)
r_transform.arrange(direction=UP, buff=1)
transform.arrange(direction=UP, buff=1)
ints.arrange(buff=2)
self.add(ints, texts)
# The mobs replace each other and none are left behind
self.play(ReplacementTransform(r_transform[0], r_transform[1]))
self.play(ReplacementTransform(r_transform[1], r_transform[2]))
self.play(ReplacementTransform(r_transform[2], r_transform[3]))
# The mobs linger after the Transform()
self.play(Transform(transform[0], transform[1]))
self.play(Transform(transform[1], transform[2]))
self.play(Transform(transform[2], transform[3]))
self.wait()
When I run it, this happens:
- The 1 goes to 2, leaving nothing behind.
- The 2 goes to 3, leaving nothing behind.
- The 3 goes to 4, leaving another 3 behind (just like a regular
Transform()
).
Why? And more importantly: how do I move the 3 without leaving anything behind?
NOTE: I have a fresh reinstall with python 3.13.1 and manim community 0.19.0, they are the latest versions.
Share Improve this question asked Jan 24 at 10:02 SteeveDrozSteeveDroz 6,1366 gold badges35 silver badges69 bronze badges1 Answer
Reset to default 1 +100Explanation
The problem is that the ReplacementTransform
of an Mobject
that is on the
scene with another Mobject
that is already on the scene is not OK;
the way ReplacementTransform
is currently implemented assumes the
new Mobject
is not on the scene. That is indeed surprising, since your code
is based on the one and only example in the documentation
for ReplacementTransform
There are two positions in the source code that are relevant for this:
ReplacementTrasnform(mobject, target_mobject)
is essentially aTransform(mobject, target_mboject)
followed by ascene.replace(mobject, target_mobject)
, see transform.py#L214, with transform.py#L294. Indeed, for all intents and purposes, in the original source code,
can be substituted byself.play(ReplacementTransform(r_transform[0], r_transform[1]))
self.play(Transform(r_transform[0], r_transform[1])) self.replace(r_transform[0], r_transform[1])
self
being here theScene
(the time of the call toreplace
might be different but the result is the same).- The function
Scene#replace(old_object, new_object)
will just look in the scene.mobjects
list and when it findsold_object
, it putsnew_object
at the same index, see scene.py#L560.
This means that if new_object
is already on the scene, a Mobject
will be duplicated, added at the position of old_object
, but also
kept at its original position.
Indeed, in your scene (the ReplacementTransform
part), you'll have
the Integer
s:
initially: 1 2 3 4
after step1: 2 2 3 4
after step2: 3 2 3 4
finally: 4 2 3 4
The following change to your code only contains the ReplacemtTransform
part
and add an on-scene debug text that displays those Integer
s and their
y
coordinates at each of these steps:
from manim import *
class ReplacementTransformOrTransform(Scene):
def construct(self):
# set up the numbers
r_transform = VGroup(*[Integer(i) for i in range(1, 5)])
r_transform.arrange(direction=UP, buff=1)
# step 0
self.add(r_transform)
fm = self.mobjects[0] # get_mobject_family_members()
self.add_integers_debug_text(fm, "step 0: ", 0, -3.5)
# step 1
self.play(ReplacementTransform(r_transform[0], r_transform[1]))
self.add_integers_debug_text(fm, "step 1: ", 0, -3)
# step 2
self.play(ReplacementTransform(r_transform[1], r_transform[2]))
self.add_integers_debug_text(fm, "step 2: ", 0, -2.5)
# step 3
self.play(ReplacementTransform(r_transform[2], r_transform[3]))
self.add_integers_debug_text(fm, "step 3: ", 0, -2)
self.wait()
def add_integers_debug_text(self, fm, prefix ="", x = 0, y = 0):
self.add(Text(prefix +
" | ".join([str(fmi.number)+'@y='+format(fmi.get_y(), ".2f") for fmi in fm if type(fmi).__name__ == 'Integer']),
font_size=22, font='monospace').set_x(x).set_y(y))
Here's the static final image that, if read from bottom up and left to right, explains why we see what we see at each step.
The change in coordinates for some of the Mobjects
are the obvious result
of the transform itself: 2
is raised from y = -0.67
to y = 0.66
in step 2,
and 3
is raised from y = 0.66
to y = 2
in step 3, and since the
duplicates are not clones but the same objects, those values repeat to
the duplicates.
It's quite obvious, the result is a mess.
Possible solutions
A. New Mobject
s are not on the scene
Avoid the situation where there's the new object is already on the scene:
class ReplacementTransformOrTransform(Scene):
def construct(self):
# set up the numbers
r_transforms = [VGroup(*[Integer(i) for i in range(j, 5)]) for j in range(1, 5)]
for r_transform in r_transforms:
r_transform.arrange(direction=UP, buff=1).to_edge(UP, buff=1.5)
# step 0
self.add(r_transforms[0])
# step 1
self.play(ReplacementTransform(r_transforms[0], r_transforms[1]))
# step 2
self.play(ReplacementTransform(r_transforms[1], r_transforms[2]))
# step 3
self.play(ReplacementTransform(r_transforms[2], r_transforms[3]))
self.wait()
This might be excessive for complicated scenes and for the type of
transforms addressed here, since it involves multiplication of all
objects of the VGroup
that are not affected by the transform. In the
example above there are 4 + 3 + 2 + 1 Integer
s constructed instead of the
original 4.
B. Use Scene#remove
Call self.remove(mobject)
(self
being the Scene
) after each transform
step to fully remove the Mobject
that are replaced:
class ReplacementTransformOrTransform(Scene):
def construct(self):
# set up the numbers
r_transform = VGroup(*[Integer(i) for i in range(1, 5)])
r_transform.arrange(direction=UP, buff=1)
# step 0
self.add(r_transform)
# step 1
self.play(ReplacementTransform(r_transform[0], r_transform[1]))
self.remove(r_transform[0])
# step 2
self.play(ReplacementTransform(r_transform[1], r_transform[2]))
self.remove(r_transform[1])
# step 3
self.play(ReplacementTransform(r_transform[2], r_transform[3]))
self.remove(r_transform[2])
self.wait()
Note that in self.remove(r_transform[0])
, r_transform[0]
which is
the Integer
for 1
is no longer in the scene since it was removed by
replace
as shown in the first section. This is to remove
doesn't
complain if the object is not found.
In any case, the call of both remove
and replace
(by the ReplacementTransform
)
is redundant, and indeed with remove
, the same effect can be obtained
by using Transform
everywhere instead of ReplacementTransform
.