feat(core): add Camera abstraction for viewport scrolling

- Add Camera class with modes: vertical, horizontal, omni, floating
- Refactor scroll.py and demo to use Camera abstraction
- Add vis_offset for horizontal scrolling support
- Add camera_x to EffectContext for effects
- Add pygame window resize handling
- Add HUD effect plugin for demo mode
- Add --demo flag to run demo mode
- Add tests for Camera and vis_offset
This commit is contained in:
2026-03-16 01:46:21 -07:00
parent e1408dcf16
commit 9b139a40f7
10 changed files with 343 additions and 37 deletions

View File

@@ -16,6 +16,7 @@ from engine.effects import (
firehose_line,
glitch_bar,
noise,
vis_offset,
vis_trunc,
)
from engine.render import big_wrap, lr_gradient, lr_gradient_opposite
@@ -94,16 +95,18 @@ def render_message_overlay(
def render_ticker_zone(
active: list,
scroll_cam: int,
ticker_h: int,
w: int,
noise_cache: dict,
grad_offset: float,
camera_x: int = 0,
ticker_h: int = 0,
w: int = 80,
noise_cache: dict | None = None,
grad_offset: float = 0.0,
) -> tuple[list[str], dict]:
"""Render the ticker scroll zone.
Args:
active: list of (content_rows, color, canvas_y, meta_idx)
scroll_cam: camera position (viewport top)
camera_x: horizontal camera offset
ticker_h: height of ticker zone
w: terminal width
noise_cache: dict of cy -> noise string
@@ -112,6 +115,8 @@ def render_ticker_zone(
Returns:
(list of ANSI strings, updated noise_cache)
"""
if noise_cache is None:
noise_cache = {}
buf = []
top_zone = max(1, int(ticker_h * 0.25))
bot_zone = max(1, int(ticker_h * 0.10))
@@ -137,7 +142,7 @@ def render_ticker_zone(
colored = lr_gradient([raw], grad_offset)[0]
else:
colored = raw
ln = vis_trunc(colored, w)
ln = vis_trunc(vis_offset(colored, camera_x), w)
if row_fade < 1.0:
ln = fade_line(ln, row_fade)
@@ -228,11 +233,12 @@ def process_effects(
h: int,
scroll_cam: int,
ticker_h: int,
mic_excess: float,
grad_offset: float,
frame_number: int,
has_message: bool,
items: list,
camera_x: int = 0,
mic_excess: float = 0.0,
grad_offset: float = 0.0,
frame_number: int = 0,
has_message: bool = False,
items: list | None = None,
) -> list[str]:
"""Process buffer through effect chain."""
if _effect_chain is None:
@@ -242,12 +248,13 @@ def process_effects(
terminal_width=w,
terminal_height=h,
scroll_cam=scroll_cam,
camera_x=camera_x,
ticker_height=ticker_h,
mic_excess=mic_excess,
grad_offset=grad_offset,
frame_number=frame_number,
has_message=has_message,
items=items,
items=items or [],
)
return _effect_chain.process(buf, ctx)