Animation
Property interpolation over time, created with the animate() method.
Overview
There is no constructible mcrfpy.Animation class. Animations are created by calling animate() on the object you want to animate: every drawable (Frame, Caption, Sprite, Grid, Line, Circle, Arc, Viewport3D) and Entity exposes it. The call creates the animation, starts it, and registers it with the engine’s animation manager in one step, then returns an Animation handle you can use to monitor or cancel it.
The handle’s type displays as mcrfpy.Animation, but the class is not exported from the module — the only way to obtain one is as the return value of animate().
Quick Reference
import mcrfpy
scene = mcrfpy.Scene("demo")
mcrfpy.current_scene = scene
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
scene.children.append(frame)
# Animate a property: creates, starts, and registers the animation
frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_OUT_QUAD)
# With a completion callback
def on_done(target, prop, value):
print(f"{prop} finished at {value}")
frame.animate("opacity", 0.0, 1.0, callback=on_done)
# The returned Animation handle can be inspected and controlled
anim = frame.animate("w", 300.0, 1.5)
print(anim.property, anim.duration, anim.is_complete)
anim.stop() # cancel without applying the final value
The animate() Method
obj.animate(property, target, duration, easing=None, delta=False,
loop=False, callback=None, conflict_mode='replace') -> Animation
| Parameter | Type | Default | Description |
|---|---|---|---|
property |
str | required | Name of the property to animate (see Animatable Properties) |
target |
varies | required | Target value; type depends on the property (see Target Value Types) |
duration |
float | required | Animation duration in seconds |
easing |
Easing / str | None | mcrfpy.Easing enum value, legacy string name (e.g. 'easeInOut'), or None for linear |
delta |
bool | False | If True, target is relative to the current value instead of absolute |
loop |
bool | False | If True, the animation repeats from the start when it reaches the end |
callback |
callable | None | Called once when the animation completes (never called for looping animations) |
conflict_mode |
str | ‘replace’ | 'replace', 'queue', or 'error' — behavior when the property is already animating |
Raises:
ValueError— the name is not an animatable attribute of this object, the easing string is unknown,conflict_modeis not one of the three accepted values, or a Color target is out of range.TypeError— the target is not of the property’s type, orcallbackis not callable.RuntimeError—conflict_mode='error'and the property is already being animated, or an Entity’sx/y/posis animated while the entity is not on a grid.
Entity.animate() takes the same arguments; only its set of valid property names differs (see below).
Callback Signature
The completion callback receives three arguments:
import mcrfpy
scene = mcrfpy.Scene("demo")
mcrfpy.current_scene = scene
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
scene.children.append(frame)
def on_arrival(target, prop, value):
# target: the Frame that was animated
# prop: "x"
# value: 500.0
print(f"{type(target).__name__}.{prop} reached {value}")
frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_OUT_CUBIC, callback=on_arrival)
The callback fires when the animation finishes naturally, or on the next frame after complete() is called on the handle. It does not fire when the animation is cancelled with stop(), and looping animations never fire it (they never complete).
Conflict Handling
Each (object, property) pair can have only one active animation. conflict_mode controls what happens when you call animate() for a property that is already animating:
| Mode | Behavior |
|---|---|
'replace' (default) |
The existing animation ends immediately (jumping to its final value) and the new one starts |
'queue' |
The new animation waits and starts when the current one finishes |
'error' |
Raises RuntimeError |
import mcrfpy
scene = mcrfpy.Scene("demo")
mcrfpy.current_scene = scene
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
scene.children.append(frame)
frame.animate("x", 100.0, 1.0)
# replace (default): the running animation jumps to its final value,
# then the new animation starts
frame.animate("x", 200.0, 1.0)
# queue: run after the current animation on "x" finishes
frame.animate("x", 300.0, 1.0, conflict_mode="queue")
# error: raise RuntimeError if "x" is already animating
try:
frame.animate("x", 400.0, 1.0, conflict_mode="error")
except RuntimeError as e:
print(e)
Notes:
- When
animate()is called from inside an animation callback, a replaced animation is cancelled in place rather than jumped to its final value. - The per-property lock is released when the finished
Animationobject is destroyed, not at the moment it completes. If you hold a reference to a completed animation, a queued animation on the same property will not start until you drop that reference. When usingconflict_mode='queue', discard handles you don’t need. - Sub-properties are locked independently of their parent:
"fill_color"and"fill_color.a"are separate keys and can animate simultaneously.
The Animation Handle
animate() returns an Animation object. All of its data attributes are read-only.
Properties
| Property | Type | Description |
|---|---|---|
property |
str | Name of the property being animated |
duration |
float | Total duration in seconds |
elapsed |
float | Seconds since the animation started (clamped to duration) |
is_complete |
bool | True when elapsed >= duration or complete() was called |
is_delta |
bool | Whether the animation uses delta (relative) mode |
is_looping |
bool | Whether the animation repeats when it reaches the end |
Methods
| Method | Description |
|---|---|
complete() |
Jump to the final value immediately; the callback, if any, fires on the next frame |
stop() |
Cancel without applying the final value; the callback is NOT fired |
get_current_value() |
Current interpolated value (type matches the property) |
hasValidTarget() |
True if the animated object still exists; animations self-clean when their target is destroyed |
start(target, conflict_mode='replace') |
Attach and start on a target object (used internally by animate(); rarely needed) |
update(delta_time) |
Advance by delta_time seconds; returns True while running (called automatically by the engine) |
import mcrfpy
scene = mcrfpy.Scene("demo")
mcrfpy.current_scene = scene
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
scene.children.append(frame)
anim = frame.animate("x", 500.0, 2.0)
print(anim.property) # "x"
print(anim.duration) # 2.0
print(anim.elapsed) # seconds since start
print(anim.is_complete) # False until finished (or complete() called)
print(anim.get_current_value()) # current interpolated value
anim.complete() # jump to x=500 immediately; a callback would fire on the next frame
print(frame.x) # 500.0
Animatable Properties
The rule (0.3.0): every name animate() accepts is a readable attribute of the object, animated in that attribute’s units and type, so animating ends exactly where assigning the same value would. frame.animate("w", 300.0, 0.5) finishes with frame.w == 300.0, just as frame.w = 300.0 does. A name that isn’t an animatable attribute raises ValueError; for names that animate() used to accept, the message names the replacement (see below).
Every drawable
All drawables (Frame, Caption, Sprite, Grid, Line, Circle, Arc, Viewport3D) share these:
| Property | Value type | Description |
|---|---|---|
x, y |
float | Position in pixels |
pos |
Vector | Position in pixels |
rotation |
float | Rotation in degrees (not Viewport3D) |
origin |
Vector | Transform origin / rotation pivot (not Viewport3D) |
opacity |
float | 0.0 (transparent) to 1.0 (opaque) |
z_index |
int | Render order |
For Circle and Arc, the position is the center. For Line it is the midpoint: assigning or animating a Line’s pos, x or y moves both endpoints.
Frame
| Property | Value type | Description |
|---|---|---|
w, h |
float | Size |
size |
Vector | Size as a pair |
fill_color, outline_color |
Color | Colors (all channels interpolate) |
fill_color.r/.g/.b/.a, outline_color.r/.g/.b/.a |
float | Individual color channels |
outline |
float | Outline thickness |
Resizing a frame that clips or caches its children (clip_children, cache_subtree) resizes its texture too, whether you assign or animate the size.
Caption
| Property | Value type | Description |
|---|---|---|
font_size |
float | Font size in points |
outline |
float | Text outline thickness |
fill_color, outline_color |
Color | Text colors, plus their .r/.g/.b/.a channels |
text |
str | Typewriter-style text transition (see Typewriter Text) |
A Caption’s w, h and size are its read-only text bounds, so they are not animatable. Animate font_size to scale text.
Sprite
| Property | Value type | Description |
|---|---|---|
sprite_index |
int or list[int] | Texture frame; a list plays a frame sequence |
scale |
float | Uniform scale factor |
scale_x, scale_y |
float | Per-axis scale |
Grid
| Property | Value type | Description |
|---|---|---|
w, h |
float | Viewport size |
size |
Vector | Viewport size as a pair |
fill_color |
Color | Background color |
center |
Vector | Camera center in pixels |
center_x, center_y |
float | Camera center components (pixels) |
zoom |
float | Camera zoom level |
camera_rotation |
float | Camera rotation in degrees |
Line, Circle, Arc
Each also has the shared properties above (x, y, pos, rotation, origin, opacity, z_index). Whole colors animate; per-channel sub-properties (e.g. color.r) are not available on these types.
| Type | Properties |
|---|---|
Line |
start, end (Vector), thickness, color |
Circle |
center (Vector), radius, outline, fill_color, outline_color |
Arc |
center (Vector), radius, start_angle, end_angle, thickness, color |
Viewport3D
w, h (float), size (Vector), fov, fog_near, fog_far (float), bg_color, fog_color (Color), plus the shared x, y, pos, opacity and z_index.
Entity
| Property | Value type | Description |
|---|---|---|
draw_pos |
Vector | Drawn position in tile coordinates: the smooth-movement property |
x, y |
float | Position in pixels (draw_pos times the grid’s cell size) |
pos |
Vector | Position in pixels |
opacity |
float | 0.0 to 1.0 |
sprite_index |
int or list[int] | Texture frame or frame sequence |
sprite_offset |
Vector | Pixel offset of the sprite within its cell (also sprite_offset_x, sprite_offset_y) |
x, y and pos exist only while the entity is on a grid, so animating them on an entity with no grid raises RuntimeError at animate() time; draw_pos works either way. Animation moves only the drawn position: the logical cell (grid_pos / cell_pos) is unchanged.
Shader uniforms ("shader.<name>") on a drawable or entity with a shader are also animatable floats.
Not animatable
These attributes are writable but deliberately not animatable:
- Entity logical state:
grid_pos,grid_x,grid_yand thecell_*aliases,tile_size/tile_width/tile_height,turn_order,sight_radius,default_behaviorandmove_speed. - Layout margins:
margin,horiz_margin,vert_margin. - Viewport3D:
cell_size.
Entity3D still uses its own animate names (world_x, rot_y, …); see issue #449.
Names removed in 0.3.0
Each of these raises ValueError naming its replacement:
| Old name | Use instead |
|---|---|
Entity draw_x, draw_y |
draw_pos (tiles), or x/y (pixels) |
Entity x, y in tile units |
draw_pos. x/y still exist, but they are now pixels, as when you read them |
Entity sprite_scale |
none: it never had an effect, because the grid’s zoom sets the sprite scale |
position |
pos |
origin_x, origin_y |
origin |
Line start_x, start_y, end_x, end_y |
start, end |
Caption size |
font_size |
Target Value Types
A target is converted the way assigning that attribute converts it:
| Property type | Accepted targets |
|---|---|
| float | an int or a float |
| int | an int; or a list of ints to step through (sprite frames; combine with loop=True) |
| Vector | a Vector, an (x, y) tuple, or a list |
| Color | a Color, or an (r, g, b) / (r, g, b, a) tuple, checked as strictly as Color(): components must be ints in 0-255 |
| str | a str (Caption text) |
A target of the wrong type raises TypeError naming the property. The completion callback’s value is a float or int, an (x, y) tuple for a Vector, or an (r, g, b, a) tuple for a Color.
Easing
Pass a mcrfpy.Easing enum value (preferred) or a legacy string name ('linear', 'easeIn', 'easeOut', 'easeInOut', 'easeInQuad', 'easeOutBounce', …). None means linear. Unknown string names raise ValueError. See Easing for the full list of enum values and guidance on choosing one.
Examples
Chained Animations
Each callback starts the next leg of the sequence:
import mcrfpy
scene = mcrfpy.Scene("demo")
mcrfpy.current_scene = scene
frame = mcrfpy.Frame(pos=(0, 0), size=(50, 50))
scene.children.append(frame)
def step2(target, prop, value):
target.animate("y", 100.0, 0.3, callback=step3)
def step3(target, prop, value):
target.animate("x", 0.0, 0.3, callback=step4)
def step4(target, prop, value):
target.animate("y", 0.0, 0.3)
# Trace a rectangle: each callback starts the next leg
frame.animate("x", 100.0, 0.3, callback=step2)
Looping Sprite Frames
A list target with loop=True cycles through frames indefinitely:
import mcrfpy
scene = mcrfpy.Scene("demo")
mcrfpy.current_scene = scene
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
sprite = mcrfpy.Sprite(texture=texture, sprite_index=84, pos=(100, 100))
scene.children.append(sprite)
# Cycle through frames 84-87 forever (0.6 seconds per cycle)
walk = sprite.animate("sprite_index", [84, 85, 86, 87], 0.6, loop=True)
# Later, to stop the loop:
walk.stop()
Camera Pan and Zoom
import mcrfpy
scene = mcrfpy.Scene("demo")
mcrfpy.current_scene = scene
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = mcrfpy.Grid(grid_size=(30, 20), pos=(0, 0), size=(480, 320), texture=texture)
scene.children.append(grid)
# grid.center is in PIXELS. To pan to tile (14, 8) with 16x16 tiles,
# target the middle of that tile:
tile_x, tile_y = 14, 8
target = (tile_x * 16 + 8, tile_y * 16 + 8)
grid.animate("center", target, 0.4, mcrfpy.Easing.EASE_OUT_CUBIC)
grid.animate("zoom", 2.0, 0.4, mcrfpy.Easing.EASE_IN_OUT)
For an instant (non-animated) camera move in tile coordinates, use grid.center_camera((tx, ty)) instead.
Smooth Entity Movement
import mcrfpy
scene = mcrfpy.Scene("demo")
mcrfpy.current_scene = scene
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(160, 160), texture=texture)
scene.children.append(grid)
player = mcrfpy.Entity(grid_pos=(1, 1), texture=texture, sprite_index=84)
grid.entities.append(player)
# Slide the drawn position across the grid in tile coordinates.
# cell_pos (the logical cell) is NOT changed by this animation.
player.animate("draw_pos", (5, 1), 0.5, mcrfpy.Easing.EASE_OUT_QUAD)
Note: entities moved by the turn manager (grid.step() with behaviors) animate their draw position automatically over entity.move_speed seconds; animating draw_pos yourself is for movement outside the turn system.
Colors and Delta Mode
import mcrfpy
scene = mcrfpy.Scene("demo")
mcrfpy.current_scene = scene
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 80))
scene.children.append(frame)
# Animate a whole color (interpolates R, G, B, and A together)
frame.animate("fill_color", (255, 0, 0, 255), 1.0)
# Animate a single channel
frame.animate("fill_color.a", 0.0, 0.5)
# Delta mode: move 50 pixels down from wherever the frame is now
frame.animate("y", 50.0, 0.5, delta=True)
Typewriter Text
Animating text transitions between strings: the first half of the duration deletes the current text character by character, the second half types out the target. With delta=True, the target is appended character by character instead.
import mcrfpy
scene = mcrfpy.Scene("demo")
mcrfpy.current_scene = scene
caption = mcrfpy.Caption(text="", pos=(20, 20))
scene.children.append(caption)
# Typewriter effect: reveals the target string over 2 seconds
caption.animate("text", "You awaken in a dark cell.", 2.0)