McRogueFace/tests/cookbook/features/demo_animation_chain.py
John McCardle 55f6ea9502 Add cookbook examples with updated callback signatures for #229, #230
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>
2026-01-28 18:58:25 -05:00

424 lines
13 KiB
Python

#!/usr/bin/env python3
"""Animation Chain/Group Demo - Complex animation orchestration
Interactive controls:
1: Run sequential chain demo
2: Run parallel group demo
3: Run callback demo
4: Run looping demo
5: Run combined demo
R: Reset all animations
ESC: Exit demo
"""
import mcrfpy
import sys
# Add parent to path for imports
sys.path.insert(0, str(__file__).rsplit('/', 2)[0])
from lib.anim_utils import (
AnimationChain, AnimationGroup, delay, callback,
fade_in, fade_out, slide_in_from_left, shake
)
class AnimationDemo:
def __init__(self):
self.scene = mcrfpy.Scene("animation_demo")
self.ui = self.scene.children
self.demo_frames = []
self.active_animations = []
self.setup()
def setup(self):
"""Build the demo scene."""
# Background
bg = mcrfpy.Frame(
pos=(0, 0),
size=(1024, 768),
fill_color=mcrfpy.Color(20, 20, 25)
)
self.ui.append(bg)
# Title
title = mcrfpy.Caption(
text="Animation Chain/Group Demo",
pos=(512, 30),
font_size=28,
fill_color=mcrfpy.Color(255, 255, 255)
)
title.outline = 2
title.outline_color = mcrfpy.Color(0, 0, 0)
self.ui.append(title)
# Create demo areas
self._create_chain_demo()
self._create_group_demo()
self._create_callback_demo()
self._create_loop_demo()
self._create_combined_demo()
# Status display
self.status = mcrfpy.Caption(
text="Press 1-5 to run demos, R to reset",
pos=(50, 700),
font_size=16,
fill_color=mcrfpy.Color(100, 200, 100)
)
self.ui.append(self.status)
# Instructions
instr = mcrfpy.Caption(
text="1: Chain | 2: Group | 3: Callback | 4: Loop | 5: Combined | R: Reset | ESC: Exit",
pos=(50, 730),
font_size=14,
fill_color=mcrfpy.Color(120, 120, 120)
)
self.ui.append(instr)
def _create_chain_demo(self):
"""Create the sequential chain demo area."""
# Label
label = mcrfpy.Caption(
text="1. Sequential Chain",
pos=(50, 80),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(label)
# Description
desc = mcrfpy.Caption(
text="Move right -> wait -> move down -> wait -> move left",
pos=(50, 100),
font_size=12,
fill_color=mcrfpy.Color(100, 100, 100)
)
self.ui.append(desc)
# Animated frame
self.chain_frame = mcrfpy.Frame(
pos=(50, 130),
size=(60, 60),
fill_color=mcrfpy.Color(100, 150, 200),
outline_color=mcrfpy.Color(150, 200, 255),
outline=2
)
self.demo_frames.append(('chain', self.chain_frame, (50, 130)))
self.ui.append(self.chain_frame)
def _create_group_demo(self):
"""Create the parallel group demo area."""
# Label
label = mcrfpy.Caption(
text="2. Parallel Group",
pos=(350, 80),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(label)
# Description
desc = mcrfpy.Caption(
text="Move + resize + change color simultaneously",
pos=(350, 100),
font_size=12,
fill_color=mcrfpy.Color(100, 100, 100)
)
self.ui.append(desc)
# Animated frame
self.group_frame = mcrfpy.Frame(
pos=(350, 130),
size=(60, 60),
fill_color=mcrfpy.Color(200, 100, 100),
outline_color=mcrfpy.Color(255, 150, 150),
outline=2
)
self.demo_frames.append(('group', self.group_frame, (350, 130)))
self.ui.append(self.group_frame)
def _create_callback_demo(self):
"""Create the callback demo area."""
# Label
label = mcrfpy.Caption(
text="3. Callbacks",
pos=(650, 80),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(label)
# Description
desc = mcrfpy.Caption(
text="Each step triggers a callback",
pos=(650, 100),
font_size=12,
fill_color=mcrfpy.Color(100, 100, 100)
)
self.ui.append(desc)
# Animated frame
self.callback_frame = mcrfpy.Frame(
pos=(650, 130),
size=(60, 60),
fill_color=mcrfpy.Color(100, 200, 100),
outline_color=mcrfpy.Color(150, 255, 150),
outline=2
)
self.demo_frames.append(('callback', self.callback_frame, (650, 130)))
self.ui.append(self.callback_frame)
# Callback counter display
self.callback_counter = mcrfpy.Caption(
text="Callbacks: 0",
pos=(720, 160),
font_size=12,
fill_color=mcrfpy.Color(100, 200, 100)
)
self.ui.append(self.callback_counter)
def _create_loop_demo(self):
"""Create the looping demo area."""
# Label
label = mcrfpy.Caption(
text="4. Looping",
pos=(50, 280),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(label)
# Description
desc = mcrfpy.Caption(
text="Continuous back-and-forth animation",
pos=(50, 300),
font_size=12,
fill_color=mcrfpy.Color(100, 100, 100)
)
self.ui.append(desc)
# Animated frame
self.loop_frame = mcrfpy.Frame(
pos=(50, 330),
size=(40, 40),
fill_color=mcrfpy.Color(200, 200, 100),
outline_color=mcrfpy.Color(255, 255, 150),
outline=2
)
self.demo_frames.append(('loop', self.loop_frame, (50, 330)))
self.ui.append(self.loop_frame)
self.loop_chain = None
def _create_combined_demo(self):
"""Create the combined demo area."""
# Label
label = mcrfpy.Caption(
text="5. Combined (Chain of Groups)",
pos=(350, 280),
font_size=16,
fill_color=mcrfpy.Color(150, 150, 150)
)
self.ui.append(label)
# Description
desc = mcrfpy.Caption(
text="Multiple frames animating in complex patterns",
pos=(350, 300),
font_size=12,
fill_color=mcrfpy.Color(100, 100, 100)
)
self.ui.append(desc)
# Multiple animated frames
self.combined_frames = []
colors = [
mcrfpy.Color(200, 100, 150),
mcrfpy.Color(150, 100, 200),
mcrfpy.Color(100, 150, 200),
]
for i, color in enumerate(colors):
frame = mcrfpy.Frame(
pos=(350 + i * 70, 330),
size=(50, 50),
fill_color=color,
outline=1
)
self.combined_frames.append(frame)
self.demo_frames.append(('combined', frame, (350 + i * 70, 330)))
self.ui.append(frame)
def run_chain_demo(self):
"""Run the sequential chain demo."""
self.status.text = "Running: Sequential Chain"
chain = AnimationChain(
(self.chain_frame, "x", 200, 0.5),
delay(0.3),
(self.chain_frame, "y", 200, 0.5),
delay(0.3),
(self.chain_frame, "x", 50, 0.5),
callback=lambda: setattr(self.status, 'text', 'Chain complete!')
)
chain.start()
self.active_animations.append(chain)
def run_group_demo(self):
"""Run the parallel group demo."""
self.status.text = "Running: Parallel Group"
group = AnimationGroup(
(self.group_frame, "x", 500, 1.0),
(self.group_frame, "w", 100, 1.0),
(self.group_frame, "h", 100, 1.0),
callback=lambda: setattr(self.status, 'text', 'Group complete!')
)
group.start()
self.active_animations.append(group)
def run_callback_demo(self):
"""Run the callback demo."""
self.status.text = "Running: Callbacks"
self.callback_count = 0
def increment_counter():
self.callback_count += 1
self.callback_counter.text = f"Callbacks: {self.callback_count}"
chain = AnimationChain(
callback(increment_counter),
(self.callback_frame, "x", 750, 0.3),
callback(increment_counter),
delay(0.2),
callback(increment_counter),
(self.callback_frame, "y", 200, 0.3),
callback(increment_counter),
(self.callback_frame, "x", 650, 0.3),
callback(increment_counter),
callback=lambda: setattr(self.status, 'text', f'Callback demo complete! ({self.callback_count} callbacks)')
)
chain.start()
self.active_animations.append(chain)
def run_loop_demo(self):
"""Run the looping demo."""
self.status.text = "Running: Looping (press R to stop)"
# Stop any existing loop
if self.loop_chain:
self.loop_chain.stop()
self.loop_chain = AnimationChain(
(self.loop_frame, "x", 250, 1.0),
(self.loop_frame, "x", 50, 1.0),
loop=True
)
self.loop_chain.start()
self.active_animations.append(self.loop_chain)
def run_combined_demo(self):
"""Run the combined demo with chain of groups."""
self.status.text = "Running: Combined"
# First, all frames slide down together
# Then, each one bounces back up sequentially
frame1, frame2, frame3 = self.combined_frames
chain = AnimationChain(
# Phase 1: All move down together (as a group effect via separate chains)
(frame1, "y", 450, 0.5),
delay(0.1),
(frame2, "y", 450, 0.5),
delay(0.1),
(frame3, "y", 450, 0.5),
delay(0.5),
# Phase 2: Spread out horizontally
(frame1, "x", 320, 0.3),
(frame3, "x", 520, 0.3),
delay(0.5),
# Phase 3: All return home
(frame1, "x", 350, 0.3),
(frame1, "y", 330, 0.3),
delay(0.1),
(frame2, "y", 330, 0.3),
delay(0.1),
(frame3, "x", 490, 0.3),
(frame3, "y", 330, 0.3),
callback=lambda: setattr(self.status, 'text', 'Combined demo complete!')
)
chain.start()
self.active_animations.append(chain)
def reset_all(self):
"""Reset all animations to initial state."""
# Stop all active animations
for anim in self.active_animations:
if hasattr(anim, 'stop'):
anim.stop()
self.active_animations.clear()
if self.loop_chain:
self.loop_chain.stop()
self.loop_chain = None
# Reset positions
for name, frame, (orig_x, orig_y) in self.demo_frames:
frame.x = orig_x
frame.y = orig_y
if name == 'group':
frame.w = 60
frame.h = 60
# Reset callback counter
self.callback_count = 0
self.callback_counter.text = "Callbacks: 0"
self.status.text = "All animations reset"
def on_key(self, key, state):
"""Handle keyboard input."""
if state != "start":
return
if key == "Escape":
sys.exit(0)
elif key == "Num1":
self.run_chain_demo()
elif key == "Num2":
self.run_group_demo()
elif key == "Num3":
self.run_callback_demo()
elif key == "Num4":
self.run_loop_demo()
elif key == "Num5":
self.run_combined_demo()
elif key == "R":
self.reset_all()
def activate(self):
"""Activate the demo scene."""
self.scene.on_key = self.on_key
mcrfpy.current_scene = self.scene
def main():
"""Run the animation demo."""
demo = AnimationDemo()
demo.activate()
# Headless mode: capture screenshot and exit
try:
if mcrfpy.headless_mode():
from mcrfpy import automation
# Run a quick demo then screenshot
demo.run_chain_demo()
mcrfpy.Timer("screenshot", lambda rt: (
automation.screenshot("screenshots/features/animation_chain_demo.png"),
sys.exit(0)
), 500)
except AttributeError:
pass
if __name__ == "__main__":
main()