From 2e6b2c48bd4d7a3fc973337b8a9c7635b65308be Mon Sep 17 00:00:00 2001 From: Gene Johnson Date: Sat, 14 Mar 2026 22:15:48 -0700 Subject: [PATCH] feat: Introduce visual effects module, enhance text rendering with SSAA, and add shifting gradient support. --- engine/config.py | 6 ++- engine/effects.py | 133 ++++++++++++++++++++++++++++++++++++++++++++++ engine/render.py | 15 +++--- 3 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 engine/effects.py diff --git a/engine/config.py b/engine/config.py index dcf762e..77d3c1f 100644 --- a/engine/config.py +++ b/engine/config.py @@ -21,10 +21,14 @@ FONT_PATH = "/Users/genejohnson/Documents/CS Bishop Drawn/CSBishopDrawn-Italic.o FONT_SZ = 60 RENDER_H = 8 # terminal rows per rendered text line +# ─── FONT RENDERING (ADVANCED) ──────────────────────────── +SSAA = 4 # super-sampling factor: render at SSAA× then downsample + # ─── SCROLL / FRAME ────────────────────────────────────── -SCROLL_DUR = 3.75 # seconds per headline +SCROLL_DUR = 5.625 # seconds per headline (2/3 original speed) FRAME_DT = 0.05 # 50ms base frame rate (20 FPS) FIREHOSE_H = 12 # firehose zone height (terminal rows) +GRAD_SPEED = 0.08 # gradient traversal speed (cycles/sec, ~12s full sweep) # ─── GLYPHS ─────────────────────────────────────────────── GLITCH = "░▒▓█▌▐╌╍╎╏┃┆┇┊┋" diff --git a/engine/effects.py b/engine/effects.py new file mode 100644 index 0000000..bad95de --- /dev/null +++ b/engine/effects.py @@ -0,0 +1,133 @@ +""" +Visual effects: noise, glitch, fade, ANSI-aware truncation, firehose, headline pool. +Depends on: config, terminal, sources. +""" + +import random +from datetime import datetime + +from engine import config +from engine.terminal import RST, DIM, G_LO, G_DIM, W_GHOST, C_DIM +from engine.sources import FEEDS, POETRY_SOURCES + + +def noise(w): + d = random.choice([0.15, 0.25, 0.35, 0.12]) + return "".join( + f"{random.choice([G_LO, G_DIM, C_DIM, W_GHOST])}" + f"{random.choice(config.GLITCH + config.KATA)}{RST}" + if random.random() < d + else " " + for _ in range(w) + ) + + +def glitch_bar(w): + c = random.choice(["░", "▒", "─", "╌"]) + n = random.randint(3, w // 2) + o = random.randint(0, w - n) + return " " * o + f"{G_LO}{DIM}" + c * n + RST + + +def fade_line(s, fade): + """Dissolve a rendered line by probabilistically dropping characters.""" + if fade >= 1.0: + return s + if fade <= 0.0: + return '' + result = [] + i = 0 + while i < len(s): + if s[i] == '\033' and i + 1 < len(s) and s[i + 1] == '[': + j = i + 2 + while j < len(s) and not s[j].isalpha(): + j += 1 + result.append(s[i:j + 1]) + i = j + 1 + elif s[i] == ' ': + result.append(' ') + i += 1 + else: + result.append(s[i] if random.random() < fade else ' ') + i += 1 + return ''.join(result) + + +def vis_trunc(s, w): + """Truncate string to visual width w, skipping ANSI escape codes.""" + result = [] + vw = 0 + i = 0 + while i < len(s): + if vw >= w: + break + if s[i] == '\033' and i + 1 < len(s) and s[i + 1] == '[': + j = i + 2 + while j < len(s) and not s[j].isalpha(): + j += 1 + result.append(s[i:j + 1]) + i = j + 1 + else: + result.append(s[i]) + vw += 1 + i += 1 + return ''.join(result) + + +def next_headline(pool, items, seen): + """Pull the next unique headline from pool, refilling as needed.""" + while True: + if not pool: + pool.extend(items) + random.shuffle(pool) + seen.clear() + title, src, ts = pool.pop() + sig = title.lower().strip() + if sig not in seen: + seen.add(sig) + return title, src, ts + + +def firehose_line(items, w): + """Generate one line of rapidly cycling firehose content.""" + r = random.random() + if r < 0.35: + # Raw headline text + title, src, ts = random.choice(items) + text = title[:w - 1] + color = random.choice([G_LO, G_DIM, W_GHOST, C_DIM]) + return f"{color}{text}{RST}" + elif r < 0.55: + # Dense glitch noise + d = random.choice([0.45, 0.55, 0.65, 0.75]) + return "".join( + f"{random.choice([G_LO, G_DIM, C_DIM, W_GHOST])}" + f"{random.choice(config.GLITCH + config.KATA)}{RST}" + if random.random() < d else " " + for _ in range(w) + ) + elif r < 0.78: + # Status / program output + sources = FEEDS if config.MODE == 'news' else POETRY_SOURCES + src = random.choice(list(sources.keys())) + msgs = [ + f" SIGNAL :: {src} :: {datetime.now().strftime('%H:%M:%S.%f')[:-3]}", + f" ░░ FEED ACTIVE :: {src}", + f" >> DECODE 0x{random.randint(0x1000, 0xFFFF):04X} :: {src[:24]}", + f" ▒▒ ACQUIRE :: {random.choice(['TCP', 'UDP', 'RSS', 'ATOM', 'XML'])} :: {src}", + f" {''.join(random.choice(config.KATA) for _ in range(3))} STRM " + f"{random.randint(0, 255):02X}:{random.randint(0, 255):02X}", + ] + text = random.choice(msgs)[:w - 1] + color = random.choice([G_LO, G_DIM, W_GHOST]) + return f"{color}{text}{RST}" + else: + # Headline fragment with glitch prefix + title, _, _ = random.choice(items) + start = random.randint(0, max(0, len(title) - 20)) + frag = title[start:start + random.randint(10, 35)] + pad = random.randint(0, max(0, w - len(frag) - 8)) + gp = ''.join(random.choice(config.GLITCH) for _ in range(random.randint(1, 3))) + text = (' ' * pad + gp + ' ' + frag)[:w - 1] + color = random.choice([G_LO, C_DIM, W_GHOST]) + return f"{color}{text}{RST}" diff --git a/engine/render.py b/engine/render.py index 76ee379..7b4007b 100644 --- a/engine/render.py +++ b/engine/render.py @@ -71,8 +71,11 @@ def render_line(text, fnt=None): draw = ImageDraw.Draw(img) draw.text((-bbox[0] + pad, -bbox[1] + pad), text, fill=255, font=fnt) pix_h = config.RENDER_H * 2 - scale = pix_h / max(img_h, 1) - new_w = max(1, int(img_w * scale)) + hi_h = pix_h * config.SSAA + scale = hi_h / max(img_h, 1) + new_w_hi = max(1, int(img_w * scale)) + img = img.resize((new_w_hi, hi_h), Image.Resampling.LANCZOS) + new_w = max(1, int(new_w_hi / config.SSAA)) img = img.resize((new_w, pix_h), Image.Resampling.LANCZOS) data = img.tobytes() thr = 80 @@ -129,8 +132,8 @@ def big_wrap(text, max_w, fnt=None): return out -def lr_gradient(rows): - """Color each non-space block character with a left-to-right gradient.""" +def lr_gradient(rows, offset=0.0): + """Color each non-space block character with a shifting left-to-right gradient.""" n = len(GRAD_COLS) max_x = max((len(r.rstrip()) for r in rows if r.strip()), default=1) out = [] @@ -143,7 +146,8 @@ def lr_gradient(rows): if ch == ' ': buf.append(' ') else: - idx = min(round(x / max(max_x - 1, 1) * (n - 1)), n - 1) + shifted = (x / max(max_x - 1, 1) + offset) % 1.0 + idx = min(round(shifted * (n - 1)), n - 1) buf.append(f"{GRAD_COLS[idx]}{ch}\033[0m") out.append("".join(buf)) return out @@ -165,7 +169,6 @@ def make_block(title, src, ts, w): ("\u201d",'"'), ("\u2013","-"), ("\u2014","-")]: title_up = title_up.replace(old, new) big_rows = big_wrap(title_up, w - 4, lang_font) - big_rows = lr_gradient(big_rows) hc = random.choice([ "\033[38;5;46m", # matrix green "\033[38;5;34m", # dark green