refactor: Implement fixed frame rate and precise timing for the scrolling animation loop.

This commit is contained in:
2026-03-14 16:54:33 -07:00
parent 8f95ee5df9
commit 69081344d5

View File

@@ -491,6 +491,7 @@ def _save_cache(items):
# ─── STREAM ─────────────────────────────────────────────── # ─── STREAM ───────────────────────────────────────────────
_SCROLL_DUR = 3.75 # seconds per headline _SCROLL_DUR = 3.75 # seconds per headline
_FRAME_DT = 0.05 # 50ms base frame rate (20 FPS)
FIREHOSE_H = 12 # firehose zone height (terminal rows) FIREHOSE_H = 12 # firehose zone height (terminal rows)
_mic_db = -99.0 # current mic level, written by background thread _mic_db = -99.0 # current mic level, written by background thread
_mic_stream = None _mic_stream = None
@@ -766,13 +767,14 @@ def stream(items):
fh = FIREHOSE_H if FIREHOSE else 0 fh = FIREHOSE_H if FIREHOSE else 0
sh = h - fh # scroll zone height sh = h - fh # scroll zone height
GAP = 3 # blank rows between headlines GAP = 3 # blank rows between headlines
dt = _SCROLL_DUR / (sh + 15) * (4 if FIREHOSE else 2) scroll_interval = _SCROLL_DUR / (sh + 15) * 2
# active blocks: (content_rows, color, canvas_y, meta_idx) # active blocks: (content_rows, color, canvas_y, meta_idx)
active = [] active = []
cam = 0 # viewport top in virtual canvas coords cam = 0 # viewport top in virtual canvas coords
next_y = sh # canvas-y where next block starts (off-screen bottom) next_y = sh # canvas-y where next block starts (off-screen bottom)
noise_cache = {} noise_cache = {}
scroll_accum = 0.0
def _noise_at(cy): def _noise_at(cy):
if cy not in noise_cache: if cy not in noise_cache:
@@ -780,10 +782,17 @@ def stream(items):
return noise_cache[cy] return noise_cache[cy]
while queued < HEADLINE_LIMIT or active: while queued < HEADLINE_LIMIT or active:
t0 = time.monotonic()
w, h = tw(), th() w, h = tw(), th()
fh = FIREHOSE_H if FIREHOSE else 0 fh = FIREHOSE_H if FIREHOSE else 0
sh = h - fh sh = h - fh
# Advance scroll on schedule
scroll_accum += _FRAME_DT
while scroll_accum >= scroll_interval:
scroll_accum -= scroll_interval
cam += 1
# Enqueue new headlines when room at the bottom # Enqueue new headlines when room at the bottom
while next_y < cam + sh + 10 and queued < HEADLINE_LIMIT: while next_y < cam + sh + 10 and queued < HEADLINE_LIMIT:
t, src, ts = _next_headline(pool, items, seen) t, src, ts = _next_headline(pool, items, seen)
@@ -792,6 +801,13 @@ def stream(items):
next_y += len(content) + GAP next_y += len(content) + GAP
queued += 1 queued += 1
# Prune off-screen blocks and stale noise
active = [(c, hc, by, mi) for c, hc, by, mi in active
if by + len(c) > cam]
for k in list(noise_cache):
if k < cam:
del noise_cache[k]
# Draw scroll zone # Draw scroll zone
top_zone = max(1, int(sh * 0.25)) top_zone = max(1, int(sh * 0.25))
bot_zone = max(1, int(sh * 0.10)) bot_zone = max(1, int(sh * 0.10))
@@ -841,19 +857,12 @@ def stream(items):
gi = random.randint(0, g_limit - 1) gi = random.randint(0, g_limit - 1)
buf[gi] = f"\033[{gi+1};1H{glitch_bar(w)}" buf[gi] = f"\033[{gi+1};1H{glitch_bar(w)}"
sys.stdout.write("".join(buf)) sys.stdout.buffer.write("".join(buf).encode())
sys.stdout.flush() sys.stdout.flush()
time.sleep(dt)
# Advance viewport # Precise frame timing
cam += 1 elapsed = time.monotonic() - t0
time.sleep(max(0, _FRAME_DT - elapsed))
# Prune off-screen blocks and stale noise
active = [(c, hc, by, mi) for c, hc, by, mi in active
if by + len(c) > cam]
for k in list(noise_cache):
if k < cam:
del noise_cache[k]
sys.stdout.write(CLR) sys.stdout.write(CLR)
sys.stdout.flush() sys.stdout.flush()