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:
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:
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.
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 a
Transform(mobject, target_mboject) followed by a scene.replace(mobject, target_mobject),
see transform.py#L214,
with transform.py#L294.
Indeed, for all intents and purposes, in the original source code,
self.play(ReplacementTransform(r_transform[0], r_transform[1]))
can be substituted by
self.play(Transform(r_transform[0], r_transform[1]))
self.replace(r_transform[0], r_transform[1])
self being here the Scene (the time of the call to replace might be
different but the result is the same).Scene#replace(old_object, new_object) will just look
in the scene .mobjects list and when it finds old_object, it puts
new_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 Integers:
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 Integers 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.
Mobjects are not on the sceneAvoid 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 Integers constructed instead of the
original 4.
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.
