# Figment Mode Design Spec > Periodic full-screen SVG glyph overlay with flickery animation, theme-aware coloring, and extensible physical device control. ## Overview Figment mode displays a randomly selected SVG from the `figments/` directory as a flickery, glitchy half-block terminal overlay on top of the running ticker. It appears once per minute (configurable), holds for ~4.5 seconds with a three-phase animation (progressive reveal, strobing hold, dissolve), then fades back to the ticker. Colors are randomly chosen from the existing theme gradients. The feature is designed for extensibility: a generic input protocol allows MQTT, ntfy, serial, or any other control surface to trigger figments and adjust parameters in real time. ## Architecture: Hybrid Plugin + Overlay The figment is an **EffectPlugin** for lifecycle, discovery, and configuration, but delegates rendering to a **layers-style overlay helper**. This avoids stretching the `EffectPlugin.process()` contract (which transforms line buffers) while still benefiting from the plugin system for C&C, auto-discovery, and config management. ### Component Diagram ``` +-------------------+ | FigmentTrigger | (Protocol) | - NtfyTrigger | (v1) | - MqttTrigger | (future) | - SerialTrigger | (future) +--------+----------+ | | FigmentCommand v +------------------+ +-----------------+ +----------------------+ | figment_render |<---| FigmentPlugin |--->| render_figment_ | | .py | | (EffectPlugin) | | overlay() in | | | | | | layers.py | | SVG -> PIL -> | | Timer, state | | | | half-block cache | | machine, SVG | | ANSI cursor-position | | | | selection | | commands for overlay | +------------------+ +-----------------+ +----------------------+ | | get_figment_state() v +-------------------+ | scroll.py | | (5-line block) | +-------------------+ ``` ## Section 1: SVG Rasterization **File: `engine/figment_render.py`** Reuses the same PIL-based half-block encoding that `engine/render.py` uses for OTF fonts. ### Pipeline 1. **Load**: `cairosvg.svg2png()` converts SVG to PNG bytes in memory (no temp files) 2. **Resize**: PIL scales to fit terminal — width = `tw()`, height = `th() * 2` pixels (each terminal row encodes 2 pixel rows via half-blocks) 3. **Threshold**: Convert to greyscale ("L" mode), apply binary threshold to get visible/not-visible 4. **Half-block encode**: Walk pixel pairs top-to-bottom. For each 2-row pair, emit `█` (both lit), `▀` (top only), `▄` (bottom only), or space (neither) 5. **Cache**: Results cached per `(svg_path, terminal_width, terminal_height)` — invalidated on terminal resize ### Dependency `cairosvg` added as an optional dependency in `pyproject.toml` (like `sounddevice`). Figment mode gracefully degrades if unavailable. ### Key Function ```python def rasterize_svg(svg_path: str, width: int, height: int) -> list[str]: """Convert SVG file to list of half-block terminal rows (uncolored).""" ``` ## Section 2: Figment Overlay Rendering **Integration point: `engine/layers.py`** New function following the `render_message_overlay()` pattern. ### Function Signature ```python def render_figment_overlay(figment_state: FigmentState, w: int, h: int) -> list[str]: """Return ANSI cursor-positioning commands for the current figment frame.""" ``` ### Animation Phases (~4.5 seconds total) | Phase | Duration | Behavior | |-------|----------|----------| | **Reveal** | ~1.5s | Progressive scanline fill. Each frame, a percentage of the figment's non-empty cells become visible in random block order. | | **Hold** | ~1.5s | Full figment visible. Strobes between full brightness and dimmed/partial visibility every few frames. | | **Dissolve** | ~1.5s | Inverse of reveal. Cells randomly drop out, replaced by spaces. | ### Color A random theme gradient is selected from `THEME_REGISTRY` at trigger time. Applied via `lr_gradient()` — the same function that colors headlines and messages. ### Positioning Figment is centered in the viewport. Each visible row is an ANSI `\033[row;colH` command appended to the buffer, identical to how the message overlay works. ## Section 3: FigmentPlugin (Effect Plugin) **File: `effects_plugins/figment.py`** An `EffectPlugin(ABC)` subclass that owns the figment lifecycle. ### Responsibilities - **Timer**: Tracks elapsed frames. At the configured interval (default 60s), triggers a new figment. - **SVG selection**: Randomly picks from `figments/*.svg`. Avoids repeating the last shown. - **State machine**: `idle -> reveal -> hold -> dissolve -> idle`. Tracks phase progress (0.0 to 1.0). - **Color selection**: Picks a random theme key (`"green"`, `"orange"`, `"purple"`) at trigger time. - **Rasterization**: Calls `rasterize_svg()` on trigger, caches result for the display duration. ### State Machine ``` idle ──(timer fires or trigger received)──> reveal reveal ──(progress >= 1.0)──> hold hold ──(progress >= 1.0)──> dissolve dissolve ──(progress >= 1.0)──> idle ``` ### Interface The `process()` method is a no-op (returns buffer unchanged) since rendering is handled by the overlay. The plugin exposes state via: ```python def get_figment_state(self, frame_number: int) -> FigmentState | None: """Tick the state machine and return current state, or None if idle.""" ``` This mirrors the `ntfy_poller.get_active_message()` pattern. ### EffectConfig - `enabled`: bool (default `False` — opt-in) - `intensity`: float — scales strobe frequency and reveal/dissolve speed - `params`: - `interval_secs`: 60 (time between figments) - `display_secs`: 4.5 (total animation duration) - `figment_dir`: "figments" (SVG source directory) Controllable via C&C: `/effects figment on`, `/effects figment intensity 0.7`. ## Section 4: Input Abstraction (FigmentTrigger) **File: `engine/figment_trigger.py`** ### Protocol ```python class FigmentTrigger(Protocol): def poll(self) -> FigmentCommand | None: ... ``` ### FigmentCommand ```python @dataclass class FigmentCommand: action: str # "trigger" | "set_intensity" | "set_interval" | "set_color" | "stop" value: float | str | None = None ``` ### Adapters | Adapter | Transport | Dependency | Status | |---------|-----------|------------|--------| | `NtfyTrigger` | Existing C&C ntfy topic | None (reuses ntfy) | v1 | | `MqttTrigger` | MQTT broker | `paho-mqtt` (optional) | Future | | `SerialTrigger` | USB serial | `pyserial` (optional) | Future | ### Integration The `FigmentPlugin` accepts a list of triggers. Each frame, it polls all triggers and acts on commands. Triggers are optional — if none are configured, the plugin runs on its internal timer alone. ### EventBus Bridge Any trigger can publish a `FIGMENT_TRIGGER` event to the EventBus, allowing other components to react (logging, multi-display sync). ## Section 5: Scroll Loop Integration Minimal change to `engine/scroll.py` — 5 lines following the ntfy overlay pattern: ```python # In stream() setup: figment_plugin = registry.get("figment") # In frame loop, after effects processing, before display.show(): if figment_plugin and figment_plugin.config.enabled: figment_state = figment_plugin.get_figment_state(frame_number) if figment_state is not None: figment_overlay = render_figment_overlay(figment_state, w, h) buf.extend(figment_overlay) ``` ### Overlay Priority Figment overlay appends **after** effects processing but **before** the ntfy message overlay. This means: - Ntfy messages always appear on top of figments (higher priority) - Existing glitch/noise effects run over the ticker underneath the figment ## Section 6: File Summary ### New Files | File | Purpose | |------|---------| | `effects_plugins/figment.py` | FigmentPlugin — lifecycle, timer, state machine, SVG selection | | `engine/figment_render.py` | SVG to half-block rasterization pipeline | | `engine/figment_trigger.py` | FigmentTrigger protocol, FigmentCommand, NtfyTrigger adapter | | `tests/test_figment.py` | Plugin lifecycle, state machine, timer, overlay rendering | | `tests/fixtures/test.svg` | Minimal SVG for testing rasterization | ### Modified Files | File | Change | |------|--------| | `engine/scroll.py` | 5-line figment overlay integration | | `engine/layers.py` | Add `render_figment_overlay()` function | | `pyproject.toml` | Add `cairosvg` as optional dependency | ## Testing Strategy - **Unit**: State machine transitions, timer accuracy, SVG rasterization output, FigmentCommand parsing - **Integration**: Plugin discovery, overlay rendering with mock terminal dimensions, C&C command handling - **Fixture**: Minimal `test.svg` (simple shape) for deterministic rasterization tests