From a1dcceac47f1dd5b41b4acd776d9f71a69f9e0f9 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 16 Mar 2026 02:04:53 -0700 Subject: [PATCH] feat(demo): add pipeline visualization demo mode - Add --pipeline-demo flag for ASCII pipeline animation - Create engine/pipeline_viz.py with animated pipeline graphics - Shows data flow, camera modes, FPS counter - Run with: python mainline.py --pipeline-demo --display pygame --- engine/app.py | 145 +++++++++++++++++++++++++++++++++++++++++ engine/config.py | 1 + engine/pipeline_viz.py | 123 ++++++++++++++++++++++++++++++++++ mise.toml | 1 + 4 files changed, 270 insertions(+) create mode 100644 engine/pipeline_viz.py diff --git a/engine/app.py b/engine/app.py index 1b39ad1..fb60487 100644 --- a/engine/app.py +++ b/engine/app.py @@ -558,6 +558,147 @@ def run_demo_mode(): print("\n \033[38;5;245mDemo ended\033[0m") +def run_pipeline_demo(): + """Run pipeline visualization demo mode - shows ASCII pipeline animation.""" + import time + + from engine import config + from engine.camera import Camera, CameraMode + from engine.display import DisplayRegistry + from engine.effects import ( + EffectContext, + PerformanceMonitor, + get_effect_chain, + get_registry, + set_monitor, + ) + from engine.pipeline_viz import generate_animated_pipeline + + print(" \033[1;38;5;46mMAINLINE PIPELINE DEMO\033[0m") + print(" \033[38;5;245mInitializing...\033[0m") + + import effects_plugins + + effects_plugins.discover_plugins() + + registry = get_registry() + chain = get_effect_chain() + chain.set_order(["noise", "fade", "glitch", "firehose", "hud"]) + + monitor = PerformanceMonitor() + set_monitor(monitor) + chain._monitor = monitor + + display = DisplayRegistry.create("pygame") + if not display: + print(" \033[38;5;196mFailed to create pygame display\033[0m") + sys.exit(1) + + w, h = 80, 24 + display.init(w, h) + display.clear() + + camera = Camera.vertical(speed=1.0) + + effects_to_demo = ["noise", "fade", "glitch", "firehose"] + effect_idx = 0 + effect_name = effects_to_demo[effect_idx] + effect_start_time = time.time() + current_intensity = 0.0 + ramping_up = True + + camera_modes = [ + (CameraMode.VERTICAL, "vertical"), + (CameraMode.HORIZONTAL, "horizontal"), + (CameraMode.OMNI, "omni"), + (CameraMode.FLOATING, "floating"), + ] + camera_mode_idx = 0 + camera_start_time = time.time() + + frame_number = 0 + + print(" \033[38;5;82mStarting pipeline visualization...\033[0m") + print(" \033[38;5;245mPress Ctrl+C to exit\033[0m\n") + + try: + while True: + elapsed = time.time() - effect_start_time + camera_elapsed = time.time() - camera_start_time + duration = config.DEMO_EFFECT_DURATION + + if elapsed >= duration: + effect_idx = (effect_idx + 1) % len(effects_to_demo) + effect_name = effects_to_demo[effect_idx] + effect_start_time = time.time() + elapsed = 0 + current_intensity = 0.0 + ramping_up = True + + if camera_elapsed >= duration * 2: + camera_mode_idx = (camera_mode_idx + 1) % len(camera_modes) + mode, mode_name = camera_modes[camera_mode_idx] + camera = Camera(mode=mode, speed=1.0) + camera_start_time = time.time() + camera_elapsed = 0 + + progress = elapsed / duration + if ramping_up: + current_intensity = progress + if progress >= 1.0: + ramping_up = False + else: + current_intensity = 1.0 - progress + + for effect in registry.list_all().values(): + if effect.name == effect_name: + effect.config.enabled = True + effect.config.intensity = current_intensity + elif effect.name not in ("hud",): + effect.config.enabled = False + + hud_effect = registry.get("hud") + if hud_effect: + mode_name = camera_modes[camera_mode_idx][1] + hud_effect.config.params["display_effect"] = ( + f"{effect_name} / {mode_name}" + ) + hud_effect.config.params["display_intensity"] = current_intensity + + camera.update(config.FRAME_DT) + + buf = generate_animated_pipeline(w, frame_number) + + ctx = EffectContext( + terminal_width=w, + terminal_height=h, + scroll_cam=camera.y, + ticker_height=h, + camera_x=camera.x, + mic_excess=0.0, + grad_offset=0.0, + frame_number=frame_number, + has_message=False, + items=[], + ) + + result = chain.process(buf, ctx) + display.show(result) + + new_w, new_h = display.get_dimensions() + if new_w != w or new_h != h: + w, h = new_w, new_h + + frame_number += 1 + time.sleep(1 / 60) + + except KeyboardInterrupt: + pass + finally: + display.cleanup() + print("\n \033[38;5;245mPipeline demo ended\033[0m") + + def main(): from engine import config from engine.pipeline import generate_pipeline_diagram @@ -566,6 +707,10 @@ def main(): print(generate_pipeline_diagram()) return + if config.PIPELINE_DEMO: + run_pipeline_demo() + return + if config.DEMO: run_demo_mode() return diff --git a/engine/config.py b/engine/config.py index c43475f..6aea065 100644 --- a/engine/config.py +++ b/engine/config.py @@ -244,6 +244,7 @@ WEBSOCKET_PORT = _arg_int("--websocket-port", 8765) # ─── DEMO MODE ──────────────────────────────────────────── DEMO = "--demo" in sys.argv DEMO_EFFECT_DURATION = 5.0 # seconds per effect +PIPELINE_DEMO = "--pipeline-demo" in sys.argv # ─── PIPELINE DIAGRAM ──────────────────────────────────── PIPELINE_DIAGRAM = "--pipeline-diagram" in sys.argv diff --git a/engine/pipeline_viz.py b/engine/pipeline_viz.py new file mode 100644 index 0000000..c602690 --- /dev/null +++ b/engine/pipeline_viz.py @@ -0,0 +1,123 @@ +""" +Pipeline visualization - ASCII text graphics showing the render pipeline. +""" + + +def generate_pipeline_visualization(width: int = 80, height: int = 24) -> list[str]: + """Generate ASCII visualization of the pipeline. + + Args: + width: Width of the visualization in characters + height: Height in lines + + Returns: + List of formatted strings representing the pipeline + """ + lines = [] + + for y in range(height): + line = "" + + if y == 1: + line = "╔" + "═" * (width - 2) + "╗" + elif y == 2: + line = "║" + " RENDER PIPELINE ".center(width - 2) + "║" + elif y == 3: + line = "╠" + "═" * (width - 2) + "╣" + + elif y == 5: + line = "║ SOURCES ══════════════> FETCH ═════════> SCROLL ═══> EFFECTS ═> DISPLAY" + elif y == 6: + line = "║ │ │ │ │" + elif y == 7: + line = "║ RSS Poetry Camera Terminal" + elif y == 8: + line = "║ Ntfy Cache Noise WebSocket" + elif y == 9: + line = "║ Mic Fade Pygame" + elif y == 10: + line = "║ Glitch Sixel" + elif y == 11: + line = "║ Firehose Kitty" + elif y == 12: + line = "║ Hud" + + elif y == 14: + line = "╠" + "═" * (width - 2) + "╣" + elif y == 15: + line = "║ CAMERA MODES " + remaining = width - len(line) - 1 + line += ( + "─" * (remaining // 2 - 7) + + " VERTICAL " + + "─" * (remaining // 2 - 6) + + "║" + ) + elif y == 16: + line = ( + "║ " + + "●".center(8) + + " " + + "○".center(8) + + " " + + "○".center(8) + + " " + + "○".center(8) + + " " * 20 + + "║" + ) + elif y == 17: + line = ( + "║ scroll up scroll left diagonal bobbing " + + " " * 16 + + "║" + ) + + elif y == 19: + line = "╠" + "═" * (width - 2) + "╣" + elif y == 20: + fps = "60" + line = ( + f"║ FPS: {fps} │ Frame: 16.7ms │ Effects: 5 active │ Camera: VERTICAL " + + " " * (width - len(line) - 2) + + "║" + ) + + elif y == 21: + line = "╚" + "═" * (width - 2) + "╝" + + else: + line = " " * width + + lines.append(line) + + return lines + + +def generate_animated_pipeline(width: int = 80, frame: int = 0) -> list[str]: + """Generate animated ASCII visualization. + + Args: + width: Width of the visualization + frame: Animation frame number + + Returns: + List of formatted strings + """ + lines = generate_pipeline_visualization(width, 20) + + anim_chars = ["▓", "▒", "░", " ", "▓", "▒", "░"] + char = anim_chars[frame % len(anim_chars)] + + for i, line in enumerate(lines): + if "Effects" in line: + lines[i] = line.replace("═" * 5, char * 5) + + if "FPS:" in line: + lines[i] = ( + f"║ FPS: {60 - frame % 10} │ Frame: {16 + frame % 5:.1f}ms │ Effects: {5 - (frame % 3)} active │ Camera: {['VERTICAL', 'HORIZONTAL', 'OMNI', 'FLOATING'][frame % 4]} " + + " " * (80 - len(lines[i]) - 2) + + "║" + ) + + return lines diff --git a/mise.toml b/mise.toml index bfbf986..449b13b 100644 --- a/mise.toml +++ b/mise.toml @@ -40,6 +40,7 @@ run-both = { run = "uv run mainline.py --display both", depends = ["sync-all"] } run-client = { run = "mise run run-both & sleep 2 && $(open http://localhost:8766 2>/dev/null || xdg-open http://localhost:8766 2>/dev/null || echo 'Open http://localhost:8766 manually'); wait", depends = ["sync-all"] } run-demo = { run = "uv run mainline.py --demo --display pygame", depends = ["sync-all"] } run-pipeline = "uv run mainline.py --pipeline-diagram" +run-pipeline-demo = { run = "uv run mainline.py --pipeline-demo --display pygame", depends = ["sync-all"] } # ===================== # Command & Control