McRogueFace/tests/cookbook/features/demo_shaders.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

340 lines
9.9 KiB
Python

#!/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()