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

@@ -7,6 +7,7 @@ import random
import time
from engine import config
from engine.camera import Camera
from engine.display import (
Display,
TerminalDisplay,
@@ -27,10 +28,19 @@ from engine.viewport import th, tw
USE_EFFECT_CHAIN = True
def stream(items, ntfy_poller, mic_monitor, display: Display | None = None):
def stream(
items,
ntfy_poller,
mic_monitor,
display: Display | None = None,
camera: Camera | None = None,
):
"""Main render loop with four layers: message, ticker, scroll motion, firehose."""
if display is None:
display = TerminalDisplay()
if camera is None:
camera = Camera.vertical()
random.shuffle(items)
pool = list(items)
seen = set()
@@ -46,7 +56,6 @@ def stream(items, ntfy_poller, mic_monitor, display: Display | None = None):
scroll_step_interval = calculate_scroll_step(config.SCROLL_DUR, ticker_view_h)
active = []
scroll_cam = 0
ticker_next_y = ticker_view_h
noise_cache = {}
scroll_motion_accum = 0.0
@@ -72,10 +81,10 @@ def stream(items, ntfy_poller, mic_monitor, display: Display | None = None):
scroll_motion_accum += config.FRAME_DT
while scroll_motion_accum >= scroll_step_interval:
scroll_motion_accum -= scroll_step_interval
scroll_cam += 1
camera.update(config.FRAME_DT)
while (
ticker_next_y < scroll_cam + ticker_view_h + 10
ticker_next_y < camera.y + ticker_view_h + 10
and queued < config.HEADLINE_LIMIT
):
from engine.effects import next_headline
@@ -88,17 +97,17 @@ def stream(items, ntfy_poller, mic_monitor, display: Display | None = None):
queued += 1
active = [
(c, hc, by, mi) for c, hc, by, mi in active if by + len(c) > scroll_cam
(c, hc, by, mi) for c, hc, by, mi in active if by + len(c) > camera.y
]
for k in list(noise_cache):
if k < scroll_cam:
if k < camera.y:
del noise_cache[k]
grad_offset = (time.monotonic() * config.GRAD_SPEED) % 1.0
ticker_buf_start = len(buf)
ticker_buf, noise_cache = render_ticker_zone(
active, scroll_cam, ticker_h, w, noise_cache, grad_offset
active, camera.y, camera.x, ticker_h, w, noise_cache, grad_offset
)
buf.extend(ticker_buf)
@@ -110,8 +119,9 @@ def stream(items, ntfy_poller, mic_monitor, display: Display | None = None):
buf,
w,
h,
scroll_cam,
camera.y,
ticker_h,
camera.x,
mic_excess,
grad_offset,
frame_number,