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
424
tests/cookbook/features/demo_animation_chain.py
Normal file
424
tests/cookbook/features/demo_animation_chain.py
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
#!/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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue