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>
This commit is contained in:
John McCardle 2026-01-28 18:58:25 -05:00
commit 55f6ea9502
41 changed files with 8493 additions and 0 deletions

View 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
"""

View 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()

View 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()

View 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()