261 lines
8.3 KiB
Markdown
261 lines
8.3 KiB
Markdown
# Updaters and Value Trackers
|
|
|
|
## The problem updaters solve
|
|
|
|
Normal animations are discrete: `self.play()` goes from state A to state B. But what if you need continuous relationships — a label that always hovers above a moving dot, or a line that always connects two points?
|
|
|
|
Without updaters, you'd manually reposition every dependent object before every `self.play()`. Five animations that move a dot means five manual repositioning calls for the label. Miss one and it freezes in the wrong spot.
|
|
|
|
Updaters let you declare a relationship ONCE. Manim calls the updater function EVERY FRAME (15-60 fps depending on quality) to enforce that relationship, no matter what else is happening.
|
|
|
|
## ValueTracker: an invisible steering wheel
|
|
|
|
A ValueTracker is an invisible Mobject that holds a single float. It never appears on screen. It exists so you can ANIMATE it while other objects REACT to its value.
|
|
|
|
Think of it as a slider: drag the slider from 0 to 5, and every object wired to it responds in real time.
|
|
|
|
```python
|
|
tracker = ValueTracker(0) # invisible, stores 0.0
|
|
tracker.get_value() # read: 0.0
|
|
tracker.set_value(5) # write: jump to 5.0 instantly
|
|
tracker.animate.set_value(5) # animate: smoothly interpolate to 5.0
|
|
```
|
|
|
|
### The three-step pattern
|
|
|
|
Every ValueTracker usage follows this:
|
|
|
|
1. **Create the tracker** (the invisible slider)
|
|
2. **Create visible objects that READ the tracker** via updaters
|
|
3. **Animate the tracker** — all dependents update automatically
|
|
|
|
```python
|
|
# Step 1: Create tracker
|
|
x_tracker = ValueTracker(1)
|
|
|
|
# Step 2: Create dependent objects
|
|
dot = always_redraw(lambda: Dot(axes.c2p(x_tracker.get_value(), 0), color=YELLOW))
|
|
v_line = always_redraw(lambda: axes.get_vertical_line(
|
|
axes.c2p(x_tracker.get_value(), func(x_tracker.get_value())), color=BLUE
|
|
))
|
|
label = always_redraw(lambda: DecimalNumber(x_tracker.get_value(), font_size=24)
|
|
.next_to(dot, UP))
|
|
|
|
self.add(dot, v_line, label)
|
|
|
|
# Step 3: Animate the tracker — everything follows
|
|
self.play(x_tracker.animate.set_value(5), run_time=3)
|
|
```
|
|
|
|
## Types of updaters
|
|
|
|
### Lambda updater (most common)
|
|
|
|
Runs a function every frame, passing the mobject itself:
|
|
|
|
```python
|
|
# Label always stays above the dot
|
|
label.add_updater(lambda m: m.next_to(dot, UP, buff=0.2))
|
|
|
|
# Line always connects two points
|
|
line.add_updater(lambda m: m.put_start_and_end_on(
|
|
point_a.get_center(), point_b.get_center()
|
|
))
|
|
```
|
|
|
|
### Time-based updater (with dt)
|
|
|
|
The second argument `dt` is the time since the last frame (~0.017s at 60fps):
|
|
|
|
```python
|
|
# Continuous rotation
|
|
square.add_updater(lambda m, dt: m.rotate(0.5 * dt))
|
|
|
|
# Continuous rightward drift
|
|
dot.add_updater(lambda m, dt: m.shift(RIGHT * 0.3 * dt))
|
|
|
|
# Oscillation
|
|
dot.add_updater(lambda m, dt: m.move_to(
|
|
axes.c2p(m.get_center()[0], np.sin(self.time))
|
|
))
|
|
```
|
|
|
|
Use `dt` updaters for physics simulations, continuous motion, and time-dependent effects.
|
|
|
|
### always_redraw: full rebuild every frame
|
|
|
|
Creates a new mobject from scratch each frame. More expensive than `add_updater` but handles cases where the mobject's structure changes (not just position/color):
|
|
|
|
```python
|
|
# Brace that follows a resizing square
|
|
brace = always_redraw(Brace, square, UP)
|
|
|
|
# Area under curve that updates as function changes
|
|
area = always_redraw(lambda: axes.get_area(
|
|
graph, x_range=[0, x_tracker.get_value()], color=BLUE, opacity=0.3
|
|
))
|
|
|
|
# Label that reconstructs its text
|
|
counter = always_redraw(lambda: Text(
|
|
f"n = {int(x_tracker.get_value())}", font_size=24, font="Menlo"
|
|
).to_corner(UR))
|
|
```
|
|
|
|
**When to use which:**
|
|
- `add_updater` — position, color, opacity changes (cheap, preferred)
|
|
- `always_redraw` — when the shape/structure itself changes (expensive, use sparingly)
|
|
|
|
## DecimalNumber: showing live values
|
|
|
|
```python
|
|
# Counter that tracks a ValueTracker
|
|
tracker = ValueTracker(0)
|
|
number = DecimalNumber(0, font_size=48, num_decimal_places=1, color=PRIMARY)
|
|
number.add_updater(lambda m: m.set_value(tracker.get_value()))
|
|
number.add_updater(lambda m: m.next_to(dot, RIGHT, buff=0.3))
|
|
|
|
self.add(number)
|
|
self.play(tracker.animate.set_value(100), run_time=3)
|
|
```
|
|
|
|
### Variable: the labeled version
|
|
|
|
```python
|
|
var = Variable(0, Text("x", font_size=24, font="Menlo"), num_decimal_places=2)
|
|
self.add(var)
|
|
self.play(var.tracker.animate.set_value(PI), run_time=2)
|
|
# Displays: x = 3.14
|
|
```
|
|
|
|
## Removing updaters
|
|
|
|
```python
|
|
# Remove all updaters
|
|
mobject.clear_updaters()
|
|
|
|
# Suspend temporarily (during an animation that would fight the updater)
|
|
mobject.suspend_updating()
|
|
self.play(mobject.animate.shift(RIGHT))
|
|
mobject.resume_updating()
|
|
|
|
# Remove specific updater (if you stored a reference)
|
|
def my_updater(m):
|
|
m.next_to(dot, UP)
|
|
label.add_updater(my_updater)
|
|
# ... later ...
|
|
label.remove_updater(my_updater)
|
|
```
|
|
|
|
## Animation-based updaters
|
|
|
|
### UpdateFromFunc / UpdateFromAlphaFunc
|
|
|
|
These are ANIMATIONS (passed to `self.play`), not persistent updaters:
|
|
|
|
```python
|
|
# Call a function on each frame of the animation
|
|
self.play(UpdateFromFunc(mobject, lambda m: m.next_to(moving_target, UP)), run_time=3)
|
|
|
|
# With alpha (0 to 1) — useful for custom interpolation
|
|
self.play(UpdateFromAlphaFunc(circle, lambda m, a: m.set_fill(opacity=a)), run_time=2)
|
|
```
|
|
|
|
### turn_animation_into_updater
|
|
|
|
Convert a one-shot animation into a continuous updater:
|
|
|
|
```python
|
|
from manim import turn_animation_into_updater
|
|
|
|
# This would normally play once — now it loops forever
|
|
turn_animation_into_updater(Rotating(gear, rate=PI/4))
|
|
self.add(gear)
|
|
self.wait(5) # gear rotates for 5 seconds
|
|
```
|
|
|
|
## Practical patterns
|
|
|
|
### Pattern 1: Dot tracing a function
|
|
|
|
```python
|
|
tracker = ValueTracker(0)
|
|
graph = axes.plot(np.sin, x_range=[0, 2*PI], color=PRIMARY)
|
|
dot = always_redraw(lambda: Dot(
|
|
axes.c2p(tracker.get_value(), np.sin(tracker.get_value())),
|
|
color=YELLOW
|
|
))
|
|
tangent = always_redraw(lambda: axes.get_secant_slope_group(
|
|
x=tracker.get_value(), graph=graph, dx=0.01,
|
|
secant_line_color=HIGHLIGHT, secant_line_length=3
|
|
))
|
|
|
|
self.add(graph, dot, tangent)
|
|
self.play(tracker.animate.set_value(2*PI), run_time=6, rate_func=linear)
|
|
```
|
|
|
|
### Pattern 2: Live area under curve
|
|
|
|
```python
|
|
tracker = ValueTracker(0.5)
|
|
area = always_redraw(lambda: axes.get_area(
|
|
graph, x_range=[0, tracker.get_value()],
|
|
color=PRIMARY, opacity=0.3
|
|
))
|
|
area_label = always_redraw(lambda: DecimalNumber(
|
|
# Numerical integration
|
|
sum(func(x) * 0.01 for x in np.arange(0, tracker.get_value(), 0.01)),
|
|
font_size=24
|
|
).next_to(axes, RIGHT))
|
|
|
|
self.add(area, area_label)
|
|
self.play(tracker.animate.set_value(4), run_time=5)
|
|
```
|
|
|
|
### Pattern 3: Connected diagram
|
|
|
|
```python
|
|
# Nodes that can be moved, with edges that auto-follow
|
|
node_a = Dot(LEFT * 2, color=PRIMARY)
|
|
node_b = Dot(RIGHT * 2, color=SECONDARY)
|
|
edge = Line().add_updater(lambda m: m.put_start_and_end_on(
|
|
node_a.get_center(), node_b.get_center()
|
|
))
|
|
label = Text("edge", font_size=18, font="Menlo").add_updater(
|
|
lambda m: m.move_to(edge.get_center() + UP * 0.3)
|
|
)
|
|
|
|
self.add(node_a, node_b, edge, label)
|
|
self.play(node_a.animate.shift(UP * 2), run_time=2)
|
|
self.play(node_b.animate.shift(DOWN + RIGHT), run_time=2)
|
|
# Edge and label follow automatically
|
|
```
|
|
|
|
### Pattern 4: Parameter exploration
|
|
|
|
```python
|
|
# Explore how a parameter changes a curve
|
|
a_tracker = ValueTracker(1)
|
|
curve = always_redraw(lambda: axes.plot(
|
|
lambda x: a_tracker.get_value() * np.sin(x),
|
|
x_range=[0, 2*PI], color=PRIMARY
|
|
))
|
|
param_label = always_redraw(lambda: Text(
|
|
f"a = {a_tracker.get_value():.1f}", font_size=24, font="Menlo"
|
|
).to_corner(UR))
|
|
|
|
self.add(curve, param_label)
|
|
self.play(a_tracker.animate.set_value(3), run_time=3)
|
|
self.play(a_tracker.animate.set_value(0.5), run_time=2)
|
|
self.play(a_tracker.animate.set_value(1), run_time=1)
|
|
```
|
|
|
|
## Common mistakes
|
|
|
|
1. **Updater fights animation:** If a mobject has an updater that sets its position, and you try to animate it elsewhere, the updater wins every frame. Suspend updating first.
|
|
|
|
2. **always_redraw for simple moves:** If you only need to reposition, use `add_updater`. `always_redraw` reconstructs the entire mobject every frame — expensive and unnecessary for position tracking.
|
|
|
|
3. **Forgetting to add to scene:** Updaters only run on mobjects that are in the scene. `always_redraw` creates the mobject but you still need `self.add()`.
|
|
|
|
4. **Updater creates new mobjects without cleanup:** If your updater creates Text objects every frame, they accumulate. Use `always_redraw` (which handles cleanup) or update properties in-place.
|