Cookbook structure: - lib/: Reusable component library (Button, StatBar, AnimationChain, etc.) - primitives/: Demo apps for individual components - features/: Demo apps for complex features (animation chaining, shaders) - apps/: Complete mini-applications (calculator, dialogue system) - automation/: Screenshot capture utilities API signature updates applied: - on_enter/on_exit/on_move callbacks now only receive (pos) per #230 - on_cell_enter/on_cell_exit callbacks only receive (cell_pos) per #230 - Animation chain library uses Timer-based sequencing (unaffected by #229) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2daebc84b5
commit
55f6ea9502
41 changed files with 8493 additions and 0 deletions
387
tests/cookbook/lib/anim_utils.py
Normal file
387
tests/cookbook/lib/anim_utils.py
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
# McRogueFace Cookbook - Animation Utilities
|
||||
"""
|
||||
Utilities for complex animation orchestration.
|
||||
|
||||
Example:
|
||||
from lib.anim_utils import AnimationChain, AnimationGroup, delay
|
||||
|
||||
# Sequential animations
|
||||
chain = AnimationChain(
|
||||
(frame, "x", 200, 0.5),
|
||||
delay(0.2),
|
||||
(frame, "y", 300, 0.5),
|
||||
callback=on_complete
|
||||
)
|
||||
chain.start()
|
||||
|
||||
# Parallel animations
|
||||
group = AnimationGroup(
|
||||
(frame, "x", 200, 0.5),
|
||||
(frame, "opacity", 0.5, 0.5),
|
||||
callback=on_complete
|
||||
)
|
||||
group.start()
|
||||
"""
|
||||
import mcrfpy
|
||||
|
||||
|
||||
class AnimationStep:
|
||||
"""Base class for animation steps."""
|
||||
|
||||
def start(self, callback):
|
||||
"""Start the step, call callback when done."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PropertyAnimation(AnimationStep):
|
||||
"""Single property animation step.
|
||||
|
||||
Args:
|
||||
target: The UI element to animate
|
||||
property: Property name (e.g., "x", "y", "opacity")
|
||||
value: Target value
|
||||
duration: Duration in seconds
|
||||
easing: Easing function (default: EASE_OUT)
|
||||
"""
|
||||
|
||||
def __init__(self, target, property_name, value, duration, easing=None):
|
||||
self.target = target
|
||||
self.property_name = property_name
|
||||
self.value = value
|
||||
self.duration = duration
|
||||
self.easing = easing or mcrfpy.Easing.EASE_OUT
|
||||
self._timer_name = None
|
||||
|
||||
def start(self, callback=None):
|
||||
"""Start the animation."""
|
||||
# Start the animation on the target
|
||||
self.target.animate(self.property_name, self.value, self.duration, self.easing)
|
||||
|
||||
# Schedule callback after duration
|
||||
if callback:
|
||||
self._timer_name = f"anim_step_{id(self)}"
|
||||
mcrfpy.Timer(self._timer_name, lambda rt: callback(), int(self.duration * 1000))
|
||||
|
||||
|
||||
class DelayStep(AnimationStep):
|
||||
"""Delay step - just waits for a duration."""
|
||||
|
||||
def __init__(self, duration):
|
||||
self.duration = duration
|
||||
self._timer_name = None
|
||||
|
||||
def start(self, callback=None):
|
||||
"""Wait for duration then call callback."""
|
||||
if callback:
|
||||
self._timer_name = f"delay_step_{id(self)}"
|
||||
mcrfpy.Timer(self._timer_name, lambda rt: callback(), int(self.duration * 1000))
|
||||
# If no callback, this is a no-op (shouldn't happen in a chain)
|
||||
|
||||
|
||||
class CallbackStep(AnimationStep):
|
||||
"""Step that calls a function."""
|
||||
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
def start(self, callback=None):
|
||||
"""Call the function immediately, then callback."""
|
||||
self.func()
|
||||
if callback:
|
||||
callback()
|
||||
|
||||
|
||||
def delay(duration):
|
||||
"""Create a delay step.
|
||||
|
||||
Args:
|
||||
duration: Delay in seconds
|
||||
|
||||
Returns:
|
||||
DelayStep object
|
||||
"""
|
||||
return DelayStep(duration)
|
||||
|
||||
|
||||
def callback(func):
|
||||
"""Create a callback step.
|
||||
|
||||
Args:
|
||||
func: Function to call (no arguments)
|
||||
|
||||
Returns:
|
||||
CallbackStep object
|
||||
"""
|
||||
return CallbackStep(func)
|
||||
|
||||
|
||||
def _parse_animation_spec(spec):
|
||||
"""Parse an animation specification.
|
||||
|
||||
Args:
|
||||
spec: Either an AnimationStep, or a tuple of (target, property, value, duration[, easing])
|
||||
|
||||
Returns:
|
||||
AnimationStep object
|
||||
"""
|
||||
if isinstance(spec, AnimationStep):
|
||||
return spec
|
||||
|
||||
if isinstance(spec, tuple):
|
||||
if len(spec) == 4:
|
||||
target, prop, value, duration = spec
|
||||
return PropertyAnimation(target, prop, value, duration)
|
||||
elif len(spec) == 5:
|
||||
target, prop, value, duration, easing = spec
|
||||
return PropertyAnimation(target, prop, value, duration, easing)
|
||||
|
||||
raise ValueError(f"Invalid animation spec: {spec}")
|
||||
|
||||
|
||||
class AnimationChain:
|
||||
"""Sequential animation execution.
|
||||
|
||||
Runs animations one after another. Each step must complete before
|
||||
the next one starts.
|
||||
|
||||
Args:
|
||||
*steps: Animation steps - either AnimationStep objects or tuples of
|
||||
(target, property, value, duration[, easing])
|
||||
callback: Optional function to call when chain completes
|
||||
loop: If True, restart chain when it completes
|
||||
|
||||
Example:
|
||||
chain = AnimationChain(
|
||||
(frame, "x", 200, 0.5),
|
||||
delay(0.2),
|
||||
(frame, "y", 300, 0.5),
|
||||
callback=lambda: print("Done!")
|
||||
)
|
||||
chain.start()
|
||||
"""
|
||||
|
||||
def __init__(self, *steps, callback=None, loop=False):
|
||||
self.steps = [_parse_animation_spec(s) for s in steps]
|
||||
self.callback = callback
|
||||
self.loop = loop
|
||||
self._current_index = 0
|
||||
self._running = False
|
||||
|
||||
def start(self):
|
||||
"""Start the animation chain."""
|
||||
self._current_index = 0
|
||||
self._running = True
|
||||
self._run_next()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the animation chain."""
|
||||
self._running = False
|
||||
|
||||
def _run_next(self):
|
||||
"""Run the next step in the chain."""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
if self._current_index >= len(self.steps):
|
||||
# Chain complete
|
||||
if self.callback:
|
||||
self.callback()
|
||||
if self.loop:
|
||||
self._current_index = 0
|
||||
self._run_next()
|
||||
else:
|
||||
self._running = False
|
||||
return
|
||||
|
||||
# Run current step
|
||||
step = self.steps[self._current_index]
|
||||
self._current_index += 1
|
||||
step.start(callback=self._run_next)
|
||||
|
||||
|
||||
class AnimationGroup:
|
||||
"""Parallel animation execution.
|
||||
|
||||
Runs all animations simultaneously. The group completes when
|
||||
all animations have finished.
|
||||
|
||||
Args:
|
||||
*steps: Animation steps - either AnimationStep objects or tuples of
|
||||
(target, property, value, duration[, easing])
|
||||
callback: Optional function to call when all animations complete
|
||||
|
||||
Example:
|
||||
group = AnimationGroup(
|
||||
(frame, "x", 200, 0.5),
|
||||
(frame, "opacity", 0.5, 0.3),
|
||||
callback=lambda: print("All done!")
|
||||
)
|
||||
group.start()
|
||||
"""
|
||||
|
||||
def __init__(self, *steps, callback=None):
|
||||
self.steps = [_parse_animation_spec(s) for s in steps]
|
||||
self.callback = callback
|
||||
self._pending = 0
|
||||
self._running = False
|
||||
|
||||
def start(self):
|
||||
"""Start all animations in parallel."""
|
||||
if not self.steps:
|
||||
if self.callback:
|
||||
self.callback()
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._pending = len(self.steps)
|
||||
|
||||
for step in self.steps:
|
||||
step.start(callback=self._on_step_complete)
|
||||
|
||||
def stop(self):
|
||||
"""Stop tracking the group (note: individual animations continue)."""
|
||||
self._running = False
|
||||
|
||||
def _on_step_complete(self):
|
||||
"""Called when each step completes."""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self._pending -= 1
|
||||
if self._pending <= 0:
|
||||
self._running = False
|
||||
if self.callback:
|
||||
self.callback()
|
||||
|
||||
|
||||
class AnimationSequence:
|
||||
"""More complex animation sequencing with named steps and branching.
|
||||
|
||||
Args:
|
||||
steps: Dict mapping step names to animation specs or step lists
|
||||
start_step: Name of the first step to run
|
||||
callback: Optional function to call when sequence ends
|
||||
"""
|
||||
|
||||
def __init__(self, steps=None, start_step="start", callback=None):
|
||||
self.steps = steps or {}
|
||||
self.start_step = start_step
|
||||
self.callback = callback
|
||||
self._current_step = None
|
||||
self._running = False
|
||||
|
||||
def add_step(self, name, *animations, next_step=None):
|
||||
"""Add a named step.
|
||||
|
||||
Args:
|
||||
name: Step name
|
||||
*animations: Animation specs for this step
|
||||
next_step: Name of next step (or None to end)
|
||||
"""
|
||||
self.steps[name] = {
|
||||
"animations": [_parse_animation_spec(a) for a in animations],
|
||||
"next": next_step
|
||||
}
|
||||
|
||||
def start(self, step_name=None):
|
||||
"""Start the sequence from a given step."""
|
||||
self._running = True
|
||||
self._run_step(step_name or self.start_step)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the sequence."""
|
||||
self._running = False
|
||||
|
||||
def _run_step(self, name):
|
||||
"""Run a named step."""
|
||||
if not self._running or name not in self.steps:
|
||||
if name is None and self.callback:
|
||||
self.callback()
|
||||
return
|
||||
|
||||
self._current_step = name
|
||||
step_data = self.steps[name]
|
||||
animations = step_data.get("animations", [])
|
||||
next_step = step_data.get("next")
|
||||
|
||||
if not animations:
|
||||
# No animations, go directly to next
|
||||
self._run_step(next_step)
|
||||
return
|
||||
|
||||
# Run animations in parallel, then proceed to next step
|
||||
group = AnimationGroup(
|
||||
*animations,
|
||||
callback=lambda: self._run_step(next_step)
|
||||
)
|
||||
group.start()
|
||||
|
||||
|
||||
# Convenience functions for common patterns
|
||||
|
||||
def fade_in(target, duration=0.3, easing=None):
|
||||
"""Create a fade-in animation (opacity 0 to 1)."""
|
||||
target.opacity = 0
|
||||
return PropertyAnimation(target, "opacity", 1.0, duration, easing)
|
||||
|
||||
|
||||
def fade_out(target, duration=0.3, easing=None):
|
||||
"""Create a fade-out animation (current opacity to 0)."""
|
||||
return PropertyAnimation(target, "opacity", 0.0, duration, easing)
|
||||
|
||||
|
||||
def slide_in_from_left(target, distance=100, duration=0.3, easing=None):
|
||||
"""Create a slide-in from left animation."""
|
||||
original_x = target.x
|
||||
target.x = original_x - distance
|
||||
return PropertyAnimation(target, "x", original_x, duration, easing or mcrfpy.Easing.EASE_OUT)
|
||||
|
||||
|
||||
def slide_in_from_right(target, distance=100, duration=0.3, easing=None):
|
||||
"""Create a slide-in from right animation."""
|
||||
original_x = target.x
|
||||
target.x = original_x + distance
|
||||
return PropertyAnimation(target, "x", original_x, duration, easing or mcrfpy.Easing.EASE_OUT)
|
||||
|
||||
|
||||
def slide_in_from_top(target, distance=100, duration=0.3, easing=None):
|
||||
"""Create a slide-in from top animation."""
|
||||
original_y = target.y
|
||||
target.y = original_y - distance
|
||||
return PropertyAnimation(target, "y", original_y, duration, easing or mcrfpy.Easing.EASE_OUT)
|
||||
|
||||
|
||||
def slide_in_from_bottom(target, distance=100, duration=0.3, easing=None):
|
||||
"""Create a slide-in from bottom animation."""
|
||||
original_y = target.y
|
||||
target.y = original_y + distance
|
||||
return PropertyAnimation(target, "y", original_y, duration, easing or mcrfpy.Easing.EASE_OUT)
|
||||
|
||||
|
||||
def pulse(target, scale_factor=1.1, duration=0.2):
|
||||
"""Create a pulse animation (scale up then back)."""
|
||||
# Note: This requires scale animation support
|
||||
# If not available, we approximate with position
|
||||
original_x = target.x
|
||||
original_y = target.y
|
||||
offset = (scale_factor - 1) * 10 # Approximate offset
|
||||
|
||||
return AnimationChain(
|
||||
(target, "x", original_x - offset, duration / 2),
|
||||
(target, "x", original_x, duration / 2),
|
||||
)
|
||||
|
||||
|
||||
def shake(target, intensity=5, duration=0.3):
|
||||
"""Create a shake animation."""
|
||||
original_x = target.x
|
||||
step_duration = duration / 6
|
||||
|
||||
return AnimationChain(
|
||||
(target, "x", original_x - intensity, step_duration),
|
||||
(target, "x", original_x + intensity, step_duration),
|
||||
(target, "x", original_x - intensity * 0.5, step_duration),
|
||||
(target, "x", original_x + intensity * 0.5, step_duration),
|
||||
(target, "x", original_x - intensity * 0.25, step_duration),
|
||||
(target, "x", original_x, step_duration),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue