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
9
tests/cookbook/features/__init__.py
Normal file
9
tests/cookbook/features/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# McRogueFace Cookbook - Feature Demos
|
||||
"""
|
||||
Showcase demos for new and advanced features.
|
||||
|
||||
- demo_shaders.py - GLSL shader effects
|
||||
- demo_rotation.py - Transform rotation and origin
|
||||
- demo_alignment.py - 9-point alignment system
|
||||
- demo_animation_chain.py - Animation Chain/Group patterns
|
||||
"""
|
||||
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()
|
||||
454
tests/cookbook/features/demo_rotation.py
Normal file
454
tests/cookbook/features/demo_rotation.py
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Rotation Demo - Transform rotation and origin points
|
||||
|
||||
Interactive controls:
|
||||
Left/Right: Rotate selected element
|
||||
Up/Down: Adjust rotation speed
|
||||
1-4: Select element to rotate
|
||||
O: Cycle origin point
|
||||
R: Reset all rotations
|
||||
A: Toggle auto-rotation
|
||||
ESC: Exit demo
|
||||
|
||||
Rotation properties:
|
||||
- rotation: Angle in degrees
|
||||
- origin: (x, y) rotation center point
|
||||
- rotate_with_camera: For Grid entities
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
|
||||
class RotationDemo:
|
||||
def __init__(self):
|
||||
self.scene = mcrfpy.Scene("rotation_demo")
|
||||
self.ui = self.scene.children
|
||||
self.elements = []
|
||||
self.selected = 0
|
||||
self.auto_rotate = False
|
||||
self.rotation_speed = 45 # degrees per second
|
||||
self.origin_index = 0
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
"""Build the demo scene."""
|
||||
# Background
|
||||
bg = mcrfpy.Frame(
|
||||
pos=(0, 0),
|
||||
size=(1024, 768),
|
||||
fill_color=mcrfpy.Color(15, 15, 20)
|
||||
)
|
||||
self.ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(
|
||||
text="Rotation 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 rotatable elements
|
||||
self._create_frame_demo(100, 120)
|
||||
self._create_sprite_demo(400, 120)
|
||||
self._create_caption_demo(700, 120)
|
||||
self._create_origin_demo(250, 450)
|
||||
|
||||
# Control panel
|
||||
self._create_control_panel()
|
||||
|
||||
# Instructions
|
||||
instr = mcrfpy.Caption(
|
||||
text="Left/Right: Rotate | 1-4: Select | O: Cycle origin | A: Auto-rotate | R: Reset | ESC: Exit",
|
||||
pos=(50, 730),
|
||||
font_size=14,
|
||||
fill_color=mcrfpy.Color(120, 120, 120)
|
||||
)
|
||||
self.ui.append(instr)
|
||||
|
||||
# Start update timer for auto-rotation
|
||||
mcrfpy.Timer("rotation_update", self._update, 16) # ~60fps
|
||||
|
||||
def _create_frame_demo(self, x, y):
|
||||
"""Create rotating frame demo."""
|
||||
# Label
|
||||
label = mcrfpy.Caption(
|
||||
text="1. Frame Rotation",
|
||||
pos=(x + 100, y - 20),
|
||||
font_size=16,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
self.ui.append(label)
|
||||
|
||||
# Rotatable frame
|
||||
frame = mcrfpy.Frame(
|
||||
pos=(x, y),
|
||||
size=(200, 200),
|
||||
fill_color=mcrfpy.Color(80, 100, 140),
|
||||
outline_color=mcrfpy.Color(120, 150, 200),
|
||||
outline=3
|
||||
)
|
||||
|
||||
# Add child content
|
||||
inner = mcrfpy.Frame(
|
||||
pos=(50, 50),
|
||||
size=(100, 100),
|
||||
fill_color=mcrfpy.Color(100, 120, 160),
|
||||
outline=1
|
||||
)
|
||||
frame.children.append(inner)
|
||||
|
||||
content = mcrfpy.Caption(
|
||||
text="Rotates!",
|
||||
pos=(100, 85),
|
||||
font_size=14,
|
||||
fill_color=mcrfpy.Color(255, 255, 255)
|
||||
)
|
||||
frame.children.append(content)
|
||||
|
||||
self.ui.append(frame)
|
||||
self.elements.append(('Frame', frame))
|
||||
|
||||
# Rotation indicator
|
||||
angle_label = mcrfpy.Caption(
|
||||
text="0.0°",
|
||||
pos=(x + 100, y + 220),
|
||||
font_size=14,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
self.ui.append(angle_label)
|
||||
self.frame_angle_label = angle_label
|
||||
|
||||
def _create_sprite_demo(self, x, y):
|
||||
"""Create rotating sprite demo (using Frame as placeholder)."""
|
||||
# Label
|
||||
label = mcrfpy.Caption(
|
||||
text="2. Sprite Rotation",
|
||||
pos=(x + 100, y - 20),
|
||||
font_size=16,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
self.ui.append(label)
|
||||
|
||||
# Use a frame to simulate sprite (actual sprite would need texture)
|
||||
sprite_frame = mcrfpy.Frame(
|
||||
pos=(x, y),
|
||||
size=(200, 200),
|
||||
fill_color=mcrfpy.Color(100, 140, 80),
|
||||
outline_color=mcrfpy.Color(150, 200, 120),
|
||||
outline=3
|
||||
)
|
||||
|
||||
# Arrow pattern to show rotation direction
|
||||
arrow_v = mcrfpy.Caption(
|
||||
text="^",
|
||||
pos=(100, 40),
|
||||
font_size=48,
|
||||
fill_color=mcrfpy.Color(255, 255, 255)
|
||||
)
|
||||
sprite_frame.children.append(arrow_v)
|
||||
|
||||
sprite_label = mcrfpy.Caption(
|
||||
text="UP",
|
||||
pos=(100, 100),
|
||||
font_size=24,
|
||||
fill_color=mcrfpy.Color(255, 255, 255)
|
||||
)
|
||||
sprite_frame.children.append(sprite_label)
|
||||
|
||||
self.ui.append(sprite_frame)
|
||||
self.elements.append(('Sprite', sprite_frame))
|
||||
|
||||
angle_label = mcrfpy.Caption(
|
||||
text="0.0°",
|
||||
pos=(x + 100, y + 220),
|
||||
font_size=14,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
self.ui.append(angle_label)
|
||||
self.sprite_angle_label = angle_label
|
||||
|
||||
def _create_caption_demo(self, x, y):
|
||||
"""Create rotating caption demo."""
|
||||
# Label
|
||||
label = mcrfpy.Caption(
|
||||
text="3. Caption Rotation",
|
||||
pos=(x + 100, y - 20),
|
||||
font_size=16,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
self.ui.append(label)
|
||||
|
||||
# Background for visibility
|
||||
caption_bg = mcrfpy.Frame(
|
||||
pos=(x, y),
|
||||
size=(200, 200),
|
||||
fill_color=mcrfpy.Color(40, 40, 50),
|
||||
outline_color=mcrfpy.Color(80, 80, 100),
|
||||
outline=1
|
||||
)
|
||||
self.ui.append(caption_bg)
|
||||
|
||||
# Rotatable caption
|
||||
caption = mcrfpy.Caption(
|
||||
text="Rotating Text!",
|
||||
pos=(x + 100, y + 100),
|
||||
font_size=24,
|
||||
fill_color=mcrfpy.Color(255, 200, 100)
|
||||
)
|
||||
caption.outline = 2
|
||||
caption.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
self.ui.append(caption)
|
||||
self.elements.append(('Caption', caption))
|
||||
|
||||
angle_label = mcrfpy.Caption(
|
||||
text="0.0°",
|
||||
pos=(x + 100, y + 220),
|
||||
font_size=14,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
self.ui.append(angle_label)
|
||||
self.caption_angle_label = angle_label
|
||||
|
||||
def _create_origin_demo(self, x, y):
|
||||
"""Create demo showing different origin points."""
|
||||
# Label
|
||||
label = mcrfpy.Caption(
|
||||
text="4. Origin Point Demo (press O to cycle)",
|
||||
pos=(x + 200, y - 20),
|
||||
font_size=16,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
self.ui.append(label)
|
||||
|
||||
# Large frame to show origin effects
|
||||
origin_frame = mcrfpy.Frame(
|
||||
pos=(x, y),
|
||||
size=(400, 200),
|
||||
fill_color=mcrfpy.Color(140, 80, 100),
|
||||
outline_color=mcrfpy.Color(200, 120, 150),
|
||||
outline=3
|
||||
)
|
||||
|
||||
# Origin marker
|
||||
origin_marker = mcrfpy.Frame(
|
||||
pos=(0, 0),
|
||||
size=(10, 10),
|
||||
fill_color=mcrfpy.Color(255, 255, 0),
|
||||
outline=0
|
||||
)
|
||||
origin_frame.children.append(origin_marker)
|
||||
self.origin_marker = origin_marker
|
||||
|
||||
# Origin name
|
||||
origin_label = mcrfpy.Caption(
|
||||
text="Origin: Top-Left",
|
||||
pos=(200, 85),
|
||||
font_size=16,
|
||||
fill_color=mcrfpy.Color(255, 255, 255)
|
||||
)
|
||||
origin_frame.children.append(origin_label)
|
||||
self.origin_name_label = origin_label
|
||||
|
||||
self.ui.append(origin_frame)
|
||||
self.elements.append(('Origin Demo', origin_frame))
|
||||
self.origin_frame = origin_frame
|
||||
|
||||
# Origin positions to cycle through
|
||||
self.origins = [
|
||||
("Top-Left", (0, 0)),
|
||||
("Top-Center", (200, 0)),
|
||||
("Top-Right", (400, 0)),
|
||||
("Center-Left", (0, 100)),
|
||||
("Center", (200, 100)),
|
||||
("Center-Right", (400, 100)),
|
||||
("Bottom-Left", (0, 200)),
|
||||
("Bottom-Center", (200, 200)),
|
||||
("Bottom-Right", (400, 200)),
|
||||
]
|
||||
|
||||
angle_label = mcrfpy.Caption(
|
||||
text="0.0°",
|
||||
pos=(x + 200, y + 220),
|
||||
font_size=14,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
self.ui.append(angle_label)
|
||||
self.origin_angle_label = angle_label
|
||||
|
||||
def _create_control_panel(self):
|
||||
"""Create control panel showing current state."""
|
||||
panel = mcrfpy.Frame(
|
||||
pos=(750, 450),
|
||||
size=(220, 180),
|
||||
fill_color=mcrfpy.Color(30, 30, 40),
|
||||
outline_color=mcrfpy.Color(60, 60, 80),
|
||||
outline=1
|
||||
)
|
||||
self.ui.append(panel)
|
||||
|
||||
panel_title = mcrfpy.Caption(
|
||||
text="Status",
|
||||
pos=(110, 10),
|
||||
font_size=16,
|
||||
fill_color=mcrfpy.Color(200, 200, 200)
|
||||
)
|
||||
panel.children.append(panel_title)
|
||||
|
||||
self.selected_label = mcrfpy.Caption(
|
||||
text="Selected: Frame",
|
||||
pos=(15, 40),
|
||||
font_size=14,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
panel.children.append(self.selected_label)
|
||||
|
||||
self.speed_label = mcrfpy.Caption(
|
||||
text="Speed: 45°/sec",
|
||||
pos=(15, 65),
|
||||
font_size=14,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
panel.children.append(self.speed_label)
|
||||
|
||||
self.auto_label = mcrfpy.Caption(
|
||||
text="Auto-rotate: Off",
|
||||
pos=(15, 90),
|
||||
font_size=14,
|
||||
fill_color=mcrfpy.Color(200, 100, 100)
|
||||
)
|
||||
panel.children.append(self.auto_label)
|
||||
|
||||
def _update(self, runtime):
|
||||
"""Update loop for auto-rotation."""
|
||||
if self.auto_rotate:
|
||||
dt = 0.016 # Approximately 16ms per frame
|
||||
for name, element in self.elements:
|
||||
try:
|
||||
element.rotation = (element.rotation + self.rotation_speed * dt) % 360
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Update angle labels
|
||||
self._update_angle_labels()
|
||||
|
||||
def _update_angle_labels(self):
|
||||
"""Update the angle display labels."""
|
||||
labels = [self.frame_angle_label, self.sprite_angle_label,
|
||||
self.caption_angle_label, self.origin_angle_label]
|
||||
|
||||
for i, (name, element) in enumerate(self.elements):
|
||||
if i < len(labels):
|
||||
try:
|
||||
labels[i].text = f"{element.rotation:.1f}°"
|
||||
except AttributeError:
|
||||
labels[i].text = "N/A"
|
||||
|
||||
def _cycle_origin(self):
|
||||
"""Cycle to the next origin point."""
|
||||
self.origin_index = (self.origin_index + 1) % len(self.origins)
|
||||
name, pos = self.origins[self.origin_index]
|
||||
|
||||
# Update origin
|
||||
try:
|
||||
self.origin_frame.origin = pos
|
||||
self.origin_name_label.text = f"Origin: {name}"
|
||||
# Move marker to show origin position
|
||||
self.origin_marker.x = pos[0] - 5
|
||||
self.origin_marker.y = pos[1] - 5
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def on_key(self, key, state):
|
||||
"""Handle keyboard input."""
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
sys.exit(0)
|
||||
elif key == "Left":
|
||||
# Rotate left (counter-clockwise)
|
||||
name, element = self.elements[self.selected]
|
||||
try:
|
||||
element.rotation = (element.rotation - 15) % 360
|
||||
except AttributeError:
|
||||
pass
|
||||
elif key == "Right":
|
||||
# Rotate right (clockwise)
|
||||
name, element = self.elements[self.selected]
|
||||
try:
|
||||
element.rotation = (element.rotation + 15) % 360
|
||||
except AttributeError:
|
||||
pass
|
||||
elif key == "Up":
|
||||
self.rotation_speed = min(180, self.rotation_speed + 15)
|
||||
self.speed_label.text = f"Speed: {self.rotation_speed}°/sec"
|
||||
elif key == "Down":
|
||||
self.rotation_speed = max(15, self.rotation_speed - 15)
|
||||
self.speed_label.text = f"Speed: {self.rotation_speed}°/sec"
|
||||
elif key in ("Num1", "Num2", "Num3", "Num4"):
|
||||
self.selected = int(key[-1]) - 1
|
||||
if self.selected < len(self.elements):
|
||||
self.selected_label.text = f"Selected: {self.elements[self.selected][0]}"
|
||||
elif key == "O":
|
||||
self._cycle_origin()
|
||||
elif key == "A":
|
||||
self.auto_rotate = not self.auto_rotate
|
||||
if self.auto_rotate:
|
||||
self.auto_label.text = "Auto-rotate: On"
|
||||
self.auto_label.fill_color = mcrfpy.Color(100, 200, 100)
|
||||
else:
|
||||
self.auto_label.text = "Auto-rotate: Off"
|
||||
self.auto_label.fill_color = mcrfpy.Color(200, 100, 100)
|
||||
elif key == "R":
|
||||
# Reset all rotations
|
||||
for name, element in self.elements:
|
||||
try:
|
||||
element.rotation = 0
|
||||
except AttributeError:
|
||||
pass
|
||||
self.origin_index = 0
|
||||
name, pos = self.origins[0]
|
||||
try:
|
||||
self.origin_frame.origin = pos
|
||||
self.origin_name_label.text = f"Origin: {name}"
|
||||
self.origin_marker.x = pos[0] - 5
|
||||
self.origin_marker.y = pos[1] - 5
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def activate(self):
|
||||
"""Activate the demo scene."""
|
||||
self.scene.on_key = self.on_key
|
||||
mcrfpy.current_scene = self.scene
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the rotation demo."""
|
||||
demo = RotationDemo()
|
||||
demo.activate()
|
||||
|
||||
# Headless mode: capture screenshot and exit
|
||||
try:
|
||||
if mcrfpy.headless_mode():
|
||||
from mcrfpy import automation
|
||||
# Rotate elements for visual interest
|
||||
for name, element in demo.elements:
|
||||
try:
|
||||
element.rotation = 15
|
||||
except AttributeError:
|
||||
pass
|
||||
mcrfpy.Timer("screenshot", lambda rt: (
|
||||
automation.screenshot("screenshots/features/rotation_demo.png"),
|
||||
sys.exit(0)
|
||||
), 200)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
340
tests/cookbook/features/demo_shaders.py
Normal file
340
tests/cookbook/features/demo_shaders.py
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Shader Effects Demo - GLSL shader visual effects
|
||||
|
||||
Interactive controls:
|
||||
1-4: Focus on specific shader
|
||||
Space: Toggle shaders on/off
|
||||
R: Reset all shaders
|
||||
ESC: Exit demo
|
||||
|
||||
Shader uniforms available:
|
||||
- float time: Seconds since engine start
|
||||
- float delta_time: Seconds since last frame
|
||||
- vec2 resolution: Texture size in pixels
|
||||
- vec2 mouse: Mouse position in window coordinates
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
|
||||
class ShaderDemo:
|
||||
def __init__(self):
|
||||
self.scene = mcrfpy.Scene("shader_demo")
|
||||
self.ui = self.scene.children
|
||||
self.shaders_enabled = True
|
||||
self.shader_frames = []
|
||||
self.shaders = []
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
"""Build the demo scene."""
|
||||
# Background
|
||||
bg = mcrfpy.Frame(
|
||||
pos=(0, 0),
|
||||
size=(1024, 768),
|
||||
fill_color=mcrfpy.Color(15, 15, 20)
|
||||
)
|
||||
self.ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(
|
||||
text="Shader Effects 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 four shader demo quadrants
|
||||
self._create_pulse_shader(50, 100)
|
||||
self._create_vignette_shader(530, 100)
|
||||
self._create_wave_shader(50, 420)
|
||||
self._create_color_shift_shader(530, 420)
|
||||
|
||||
# Instructions
|
||||
instr = mcrfpy.Caption(
|
||||
text="1-4: Focus shader | Space: Toggle on/off | R: Reset | ESC: Exit",
|
||||
pos=(50, 730),
|
||||
font_size=14,
|
||||
fill_color=mcrfpy.Color(120, 120, 120)
|
||||
)
|
||||
self.ui.append(instr)
|
||||
|
||||
# Status
|
||||
self.status = mcrfpy.Caption(
|
||||
text="Shaders: Enabled",
|
||||
pos=(50, 700),
|
||||
font_size=16,
|
||||
fill_color=mcrfpy.Color(100, 200, 100)
|
||||
)
|
||||
self.ui.append(self.status)
|
||||
|
||||
def _create_pulse_shader(self, x, y):
|
||||
"""Create pulsing glow shader demo."""
|
||||
# Label
|
||||
label = mcrfpy.Caption(
|
||||
text="1. Pulse Glow",
|
||||
pos=(x + 200, y - 20),
|
||||
font_size=16,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
self.ui.append(label)
|
||||
|
||||
# Frame to apply shader to
|
||||
frame = mcrfpy.Frame(
|
||||
pos=(x, y),
|
||||
size=(420, 280),
|
||||
fill_color=mcrfpy.Color(80, 60, 120),
|
||||
outline_color=mcrfpy.Color(150, 100, 200),
|
||||
outline=2
|
||||
)
|
||||
|
||||
# Add some content
|
||||
content = mcrfpy.Caption(
|
||||
text="Brightness pulses\nusing time uniform",
|
||||
pos=(210, 100),
|
||||
font_size=18,
|
||||
fill_color=mcrfpy.Color(255, 255, 255)
|
||||
)
|
||||
frame.children.append(content)
|
||||
|
||||
# Create pulse shader
|
||||
pulse_shader = mcrfpy.Shader('''
|
||||
uniform sampler2D texture;
|
||||
uniform float time;
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_TexCoord[0].xy;
|
||||
vec4 color = texture2D(texture, uv);
|
||||
|
||||
// Pulse brightness
|
||||
float pulse = 0.7 + 0.3 * sin(time * 2.0);
|
||||
color.rgb *= pulse;
|
||||
|
||||
gl_FragColor = color * gl_Color;
|
||||
}
|
||||
''', dynamic=True)
|
||||
|
||||
frame.shader = pulse_shader
|
||||
self.ui.append(frame)
|
||||
self.shader_frames.append(frame)
|
||||
self.shaders.append(pulse_shader)
|
||||
|
||||
def _create_vignette_shader(self, x, y):
|
||||
"""Create vignette (darkened edges) shader demo."""
|
||||
# Label
|
||||
label = mcrfpy.Caption(
|
||||
text="2. Vignette",
|
||||
pos=(x + 200, y - 20),
|
||||
font_size=16,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
self.ui.append(label)
|
||||
|
||||
# Frame
|
||||
frame = mcrfpy.Frame(
|
||||
pos=(x, y),
|
||||
size=(420, 280),
|
||||
fill_color=mcrfpy.Color(100, 80, 60),
|
||||
outline_color=mcrfpy.Color(200, 150, 100),
|
||||
outline=2
|
||||
)
|
||||
|
||||
content = mcrfpy.Caption(
|
||||
text="Darkened edges\nclassic vignette effect",
|
||||
pos=(210, 100),
|
||||
font_size=18,
|
||||
fill_color=mcrfpy.Color(255, 255, 255)
|
||||
)
|
||||
frame.children.append(content)
|
||||
|
||||
# Create vignette shader
|
||||
vignette_shader = mcrfpy.Shader('''
|
||||
uniform sampler2D texture;
|
||||
uniform vec2 resolution;
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_TexCoord[0].xy;
|
||||
vec4 color = texture2D(texture, uv);
|
||||
|
||||
// Calculate distance from center
|
||||
vec2 center = vec2(0.5, 0.5);
|
||||
float dist = distance(uv, center);
|
||||
|
||||
// Vignette effect - darken edges
|
||||
float vignette = 1.0 - smoothstep(0.3, 0.8, dist);
|
||||
color.rgb *= vignette;
|
||||
|
||||
gl_FragColor = color * gl_Color;
|
||||
}
|
||||
''', dynamic=False)
|
||||
|
||||
frame.shader = vignette_shader
|
||||
self.ui.append(frame)
|
||||
self.shader_frames.append(frame)
|
||||
self.shaders.append(vignette_shader)
|
||||
|
||||
def _create_wave_shader(self, x, y):
|
||||
"""Create wave distortion shader demo."""
|
||||
# Label
|
||||
label = mcrfpy.Caption(
|
||||
text="3. Wave Distortion",
|
||||
pos=(x + 200, y - 20),
|
||||
font_size=16,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
self.ui.append(label)
|
||||
|
||||
# Frame
|
||||
frame = mcrfpy.Frame(
|
||||
pos=(x, y),
|
||||
size=(420, 280),
|
||||
fill_color=mcrfpy.Color(60, 100, 120),
|
||||
outline_color=mcrfpy.Color(100, 180, 220),
|
||||
outline=2
|
||||
)
|
||||
|
||||
content = mcrfpy.Caption(
|
||||
text="Sine wave\nUV distortion",
|
||||
pos=(210, 100),
|
||||
font_size=18,
|
||||
fill_color=mcrfpy.Color(255, 255, 255)
|
||||
)
|
||||
frame.children.append(content)
|
||||
|
||||
# Create wave shader
|
||||
wave_shader = mcrfpy.Shader('''
|
||||
uniform sampler2D texture;
|
||||
uniform float time;
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_TexCoord[0].xy;
|
||||
|
||||
// Apply wave distortion
|
||||
float wave = sin(uv.y * 20.0 + time * 3.0) * 0.01;
|
||||
uv.x += wave;
|
||||
|
||||
vec4 color = texture2D(texture, uv);
|
||||
gl_FragColor = color * gl_Color;
|
||||
}
|
||||
''', dynamic=True)
|
||||
|
||||
frame.shader = wave_shader
|
||||
self.ui.append(frame)
|
||||
self.shader_frames.append(frame)
|
||||
self.shaders.append(wave_shader)
|
||||
|
||||
def _create_color_shift_shader(self, x, y):
|
||||
"""Create chromatic aberration / color shift shader demo."""
|
||||
# Label
|
||||
label = mcrfpy.Caption(
|
||||
text="4. Color Shift",
|
||||
pos=(x + 200, y - 20),
|
||||
font_size=16,
|
||||
fill_color=mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
self.ui.append(label)
|
||||
|
||||
# Frame
|
||||
frame = mcrfpy.Frame(
|
||||
pos=(x, y),
|
||||
size=(420, 280),
|
||||
fill_color=mcrfpy.Color(80, 80, 80),
|
||||
outline_color=mcrfpy.Color(150, 150, 150),
|
||||
outline=2
|
||||
)
|
||||
|
||||
content = mcrfpy.Caption(
|
||||
text="Chromatic aberration\nRGB channel offset",
|
||||
pos=(210, 100),
|
||||
font_size=18,
|
||||
fill_color=mcrfpy.Color(255, 255, 255)
|
||||
)
|
||||
frame.children.append(content)
|
||||
|
||||
# Create color shift shader
|
||||
colorshift_shader = mcrfpy.Shader('''
|
||||
uniform sampler2D texture;
|
||||
uniform float time;
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_TexCoord[0].xy;
|
||||
|
||||
// Offset for each channel
|
||||
float offset = 0.005 + 0.003 * sin(time);
|
||||
|
||||
// Sample each channel with slight offset
|
||||
float r = texture2D(texture, uv + vec2(offset, 0.0)).r;
|
||||
float g = texture2D(texture, uv).g;
|
||||
float b = texture2D(texture, uv - vec2(offset, 0.0)).b;
|
||||
float a = texture2D(texture, uv).a;
|
||||
|
||||
gl_FragColor = vec4(r, g, b, a) * gl_Color;
|
||||
}
|
||||
''', dynamic=True)
|
||||
|
||||
frame.shader = colorshift_shader
|
||||
self.ui.append(frame)
|
||||
self.shader_frames.append(frame)
|
||||
self.shaders.append(colorshift_shader)
|
||||
|
||||
def toggle_shaders(self):
|
||||
"""Toggle all shaders on/off."""
|
||||
self.shaders_enabled = not self.shaders_enabled
|
||||
|
||||
for frame, shader in zip(self.shader_frames, self.shaders):
|
||||
if self.shaders_enabled:
|
||||
frame.shader = shader
|
||||
self.status.text = "Shaders: Enabled"
|
||||
self.status.fill_color = mcrfpy.Color(100, 200, 100)
|
||||
else:
|
||||
frame.shader = None
|
||||
self.status.text = "Shaders: Disabled"
|
||||
self.status.fill_color = mcrfpy.Color(200, 100, 100)
|
||||
|
||||
def on_key(self, key, state):
|
||||
"""Handle keyboard input."""
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
sys.exit(0)
|
||||
elif key == "Space":
|
||||
self.toggle_shaders()
|
||||
elif key == "R":
|
||||
# Re-enable all shaders
|
||||
self.shaders_enabled = False
|
||||
self.toggle_shaders()
|
||||
elif key in ("Num1", "Num2", "Num3", "Num4"):
|
||||
# Focus on specific shader (could zoom in)
|
||||
idx = int(key[-1]) - 1
|
||||
if idx < len(self.shader_frames):
|
||||
self.status.text = f"Focused: Shader {idx + 1}"
|
||||
|
||||
def activate(self):
|
||||
"""Activate the demo scene."""
|
||||
self.scene.on_key = self.on_key
|
||||
mcrfpy.current_scene = self.scene
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the shader demo."""
|
||||
demo = ShaderDemo()
|
||||
demo.activate()
|
||||
|
||||
# Headless mode: capture screenshot and exit
|
||||
try:
|
||||
if mcrfpy.headless_mode():
|
||||
from mcrfpy import automation
|
||||
mcrfpy.Timer("screenshot", lambda rt: (
|
||||
automation.screenshot("screenshots/features/shader_demo.png"),
|
||||
sys.exit(0)
|
||||
), 200)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue